Skip to main content

Traffic from the spec

This tutorial uses commercial gears. sim_source and sim_responder are 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.

Loading diagram…

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>:

CommandArgsEffect
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 starts
  • on_control: waits for a sim.start command via Control Plane
  • on_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:

  1. 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 in set, defaults, and templates.

  2. 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].

  3. Rate shaping in the gear or in the scenario? Base rate in scenario YAML; overridable at runtime via Control Plane API.

  4. What does a spec with no simulation section do? For sim_source, the gear refuses to start unless its own configuration gives defaults, templates or set: with neither there is nothing to generate from. sim_responder takes its defaults and rules from its configuration and needs no simulation section.

  5. Negative generation is included. The $INVALID macro generates values outside a field's closed value set, enabling adversarial traffic generation.