Skip to main content

Traffic from the spec

This tutorial uses commercial gears. Separate licences cover sim_source and sim_responder, 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. Different things played them. Robot Framework packed each message so the test could choose the card. 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. 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. Build the scenario now.

  • Demonstration. Show what a deployment does. Do not ask anyone for a connection to their scheme.

In all three the counterparty is scaffolding. 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 emits authorizations derived from the spec. You shape the rate.

  • A responder gear answers as the issuer's authorizer would. It follows rules.

Nothing about the Rack under test changes. That is the point. The scaffolding attaches at the sockets. The scenario under exercise 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. It works exactly as it would for a real scheme or a real issuer. The enrichment Rack in the middle sees traffic it cannot tell apart from the real thing.

Loading diagram…

fluxrig scenario viz generates the topology above from examples/scenarios/simulation_demo.yaml. It cannot drift from what actually runs. Click any gear to open 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. This is deliberate. It stays a single-purpose generator. The connection it dials is otherwise unread once the request leaves. Reading the reply is a separate concern from generating traffic. Checking the reply against what the request implied is separate. Counting the outcome is separate. A separate gear owns these tasks. Growing sim_source to do both mixes concerns. sim_validator, a companion gear built for exactly that, is [Roadmap]: it does not exist today.

Where the traffic comes from​

Nobody templates anything here by hand. The spec already carries the message catalog. It names which MTIs exist and how they pair. It carries the per-MTI rules. It names which fields are mandatory and which are conditional on what. It carries the value domains. It names 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 you add a field to the spec, generated traffic carries it the next run. No template needs an update.


- name: traffic
type: sim_source
config:
spec: iso8583-v87-ascii:v3.0.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 are in 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. You can replay a failed run exactly. A generator whose output you cannot reproduce 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 change the spec only for that gear:


- name: traffic
type: sim_source
config:
spec: iso8583-v87-ascii:v3.0.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.

The setup accepts both field numbers and aliases in set, defaults, and templates. Aliases (e.g., card.pan, terminal_id) survive a dialect change. Numbers match the spec directly. You can mix both.

Rate shaping at runtime​

The rate defined in the scenario is the default. You can override it 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. The gear's configuration sets its behaviour: the base delay, the default response fields and the rules. The setup echoes request fields that the response MTI admits from the request without configuration, as the spec's response_value says.

- name: authorizer
type: sim_responder
config:
spec: iso8583-v87-ascii:v3.0.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 setup does not configure the response MTI. It comes from the catalog's pairs_with. A 0100 gets a 0110 because the spec pairs those. Nobody told the responder.

delay is what makes latency testable. Set it above the enrichment's budget. The timeout path runs. Set it above the correlation entry's lifetime. The setup drops the reply. This is a real behaviour. Reproduce it deliberately rather than discovering it 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 list below resolves the draft's open questions:

  1. Fields by number or by alias? Both. Numbers match the spec directly. Aliases (e.g., card.pan, terminal_id) survive a dialect change. The setup accepts both in set, defaults, and templates.

  2. Where does a gear name a spec? A store reference (e.g., iso8583-v87-ascii:v3.0.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 lives in scenario YAML. You can override it 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. It needs no simulation section.

  5. The setup includes Negative generation. The $INVALID macro generates values outside a field's closed value set. This enables adversarial traffic generation.