Traffic from the spec
This tutorial uses commercial gears.
sim_sourceandsim_responderare licensed separately from the engine, under the Business Source License 1.1: free to read, copy and use non-production; production use, including your own traffic, needs a commercial licence. They live in github.com/jaab-tech/fluxrig-ent, a separate repository from the engine. See the enterprise introduction and the gear references for sim_source and sim_responder.
Enriching an authorization built a Rack between a card scheme and an issuer's authorizer. Exercising it needed both of those, and they were played by different things: Robot Framework packed each message so the test could choose the card, and iso8583-tool answered whatever arrived.
That is the right shape for a test suite. An independent counterparty is evidence; a counterparty made of the same code is a conversation with itself.
It is the wrong shape for three other situations, and they are common:
-
Load. Three hand-picked cards prove correctness. They say nothing about what happens at four hundred transactions per second with a realistic mix.
-
Development before the counterparty exists. The certification slot is in six weeks and the scenario has to be built now.
-
Demonstration. Showing what a deployment does, without asking anyone for a connection to their scheme.
In all three the counterparty is scaffolding. And the spec that parses the traffic already describes enough to produce it.
What you will build
The same enrichment Rack, with both ends generated:
-
a source gear that emits authorizations derived from the spec, at a rate you shape;
-
a responder gear that answers as the issuer's authorizer would, by rules.
Nothing about the Rack under test changes. That is the point: the scaffolding attaches at the sockets, and the scenario being exercised is the one that will run in production.
How the pieces connect
Both sim_source and sim_responder are logic gears: neither packs or unpacks a byte itself. A codec_iso8583 gear does that on each side, exactly as it would for a real scheme or a real issuer, so the enrichment Rack in the middle sees traffic it cannot tell apart from the real thing.
The topology above is generated from examples/scenarios/simulation_demo.yaml by fluxrig scenario viz, so it cannot drift from what actually runs. Click any gear to drill into it. The Rack under test's own gears are a schematic stand-in for the roaming-enrichment scenario, which is where its actual enrichment logic lives.
sim_source has no in port, deliberately: it stays a single-purpose generator, and the connection it dials is otherwise unread once the request leaves. Reading the reply, checking it against what the request implied, and counting the outcome is a separate concern from generating traffic in the first place, so it belongs to its own gear rather than growing sim_source into one that does both. sim_validator, a companion gear built for exactly that, is [Roadmap]: it does not exist today.
Where the traffic comes from
Nothing here is templated by hand. The spec already carries the message catalog, which MTIs exist and how they pair; the per-MTI rules, which fields are mandatory and which are conditional on what; and the value domains, which codes are legal in each coded field.
A generator that reads those produces a message that is valid by construction rather than valid because someone remembered. When a field is added to the spec, generated traffic carries it the next run, with no template to update.
- name: traffic
type: sim_source
config:
spec: iso8583-v87-ascii:v2.2.0
seed: 20260904 # every run reproduces exactly
rate:
shape: constant # constant | ramp (poisson and spike are roadmap)
tps: 200
duration: 60s
That is the whole configuration for the common case. The mix of message types, the field values and the conditional logic all come from the spec's simulation section:
x-fluxrig-simulation:
mix:
- { use: "0100", weight: 85 }
- { use: "0200", weight: 10 }
- { use: "0800", weight: 5 }
defaults:
7: $NOW
11: $STAN
37: $RRN
"0100":
2: $PAN(4111, 16) # Luhn-valid, given prefix and length
4: $RAND(100, 5000)
22: "05" # chip; the conditional rule for DE 55 fires from this
The seed is not optional. Every macro is deterministic under it, so a run that fails can be replayed exactly. A generator whose output cannot be reproduced turns an intermittent failure into an unfalsifiable story.
Overriding for one run
A load test usually wants production's shape with one thing pinned. Overrides apply on top of the spec, and only for that gear:
- name: traffic
type: sim_source
config:
spec: iso8583-v87-ascii:v2.2.0
seed: 20260904
rate: { shape: ramp, from: 10, to: 400, over: 5m }
mix:
- { use: "0100", weight: 100 } # authorizations only
set:
41: "TERM0001" # one terminal, to watch one queue
The precedence is narrow to wide: set beats the per-MTI template, which beats defaults, which beats synthesis from the field's declared domain.
Both field numbers and aliases are accepted in set, defaults, and templates. Aliases (e.g., card.pan, terminal_id) survive a dialect change; numbers match the spec directly. Mixing both is supported.
Rate shaping at runtime
The rate defined in the scenario is the default. It can be overridden at runtime via the Control Plane API:
POST /api/v1/control/sim/rate
{
"gear": "traffic",
"tps": 400,
"shape": "ramp",
"from": 10,
"to": 400,
"over": "5m"
}
This publishes a sim.rate command to the gear's control plane subject. The gear applies the new rate immediately without restart.
Runtime control commands
The gear subscribes to Control Plane commands on subject flux.ctrl.<gear-name>:
| Command | Args | Effect |
|---|---|---|
sim.start | {"gear": "name", "seed": 123} | Start generation (if trigger=on_control) |
sim.stop | {"gear": "name"} | Stop generation |
sim.rate | {"gear": "name", "tps": 400, "shape": "ramp", ...} | Change rate at runtime |
sim.reset | {"gear": "name", "seed": 1234} | Reset generator counters (STAN, RRN, named sequences) and re-seed random generator. Same seed produces identical sequences after reset. |
Start triggers
The trigger config controls when the generator starts:
on_load(default): starts immediately when the gear startson_control: waits for asim.startcommand via Control Planeon_schedule[Roadmap]: starts at a scheduled time (RFC3339 or cron). The gear fails fast at apply time if configured; it does not run today.
POST /api/v1/control/sim/start
{
"gear": "traffic",
"seed": 20260904
}
The other end
The responder answers as the authorizer. Its behaviour is set in the gear's configuration: the base delay, the default response fields and the rules. Request fields that the response MTI admits are echoed from the request without configuration, as the spec's response_value says.
- name: authorizer
type: sim_responder
config:
spec: iso8583-v87-ascii:v2.2.0
trigger: on_load
delay: 50ms
default:
39: "00" # approved
38: $AUTH
rules:
- name: Insufficient funds over limit
when: field(4) > 1000000
set: { 39: "51" }
- name: Expired card
when: field(14) < 2501
set: { 39: "54" }
A responder block in the spec's simulation section, which would give a spec its own default behaviour, is [Roadmap]. The gear does not read it today.
The response MTI is not configured: it comes from the catalog's pairs_with, so a 0100 is answered with a 0110 because the spec says those pair, not because the responder was told.
delay is what makes latency testable. Set it above the enrichment's budget and the timeout path runs; set it above the correlation entry's lifetime and the reply is lost, which is a real behaviour worth being able to reproduce deliberately rather than discovering in production.
The delay in the config is the base latency. A rule can override it with its own delay field. Rule delays replace the base delay for that message, they do not add to it.
Exercising the paths that matter
The interesting cases in the roaming tutorial were not the approvals. They were the four ways an operator can fail to answer. Rules make those reachable:
rules:
- name: Roaming mismatch declines
when: field(48) == 'RS01'
set: { 39: "59" } # suspected fraud
- name: No signal approves anyway
when: field(48) == 'RS12' || field(48) == 'RS11'
set: { 39: "00" }
Which encodes a policy decision as configuration: a missing signal does not decline. That the policy is legible in the spec, rather than buried in a simulator's Go code, is most of the value.
When not to use this
A suite where every participant is fluxrig proves less. The roaming suite keeps Robot Framework on the scheme side on purpose: an independent implementation packing the message is evidence that the wire format is right. A generated counterparty shares the codec with the thing under test, so a mutual misunderstanding of the spec passes silently.
Use generated traffic to prove your scenario behaves. Use an independent counterparty to prove you interoperate. Certification is the second, always.
Decisions made
The open questions from the draft have been resolved:
-
Fields by number or by alias? Both. Numbers match the spec directly; aliases (e.g.,
card.pan,terminal_id) survive a dialect change. Both are accepted inset,defaults, andtemplates. -
Where does a gear name a spec? A store reference (e.g.,
iso8583-v87-ascii:v2.2.0) resolved via the content-addressed store. Reading a spec from a file path is [Roadmap]. -
Rate shaping in the gear or in the scenario? Base rate in scenario YAML; overridable at runtime via Control Plane API.
-
What does a spec with no simulation section do? For
sim_source, the gear refuses to start unless its own configuration givesdefaults,templatesorset: with neither there is nothing to generate from.sim_respondertakes its defaults and rules from its configuration and needs no simulation section. -
Negative generation is included. The
$INVALIDmacro generates values outside a field's closed value set, enabling adversarial traffic generation.