Run the simulator and replay a seed¶
This replays seed 42 and prints its whole trace. A seed fixes everything about a run, so replaying it reproduces a failure exactly, as many times as you like.
The simulator lives in internal/sim. It isn't importable from outside the module; it's how the
supervisor's protocol is tested. How the simulator tested the
supervisor explains what it does and why.
Run a batch¶
With no flags this runs 20 seeds, from seed 1, each twice to check it's deterministic. The last
log line reports how many seeds passed, then how often each fault and protocol event happened
across them, as a map from event name to count: 20 seeds passed; event counts map[cancel:…
crash:… …].
-count=1 stops Go from reusing a cached pass.
| Flag | Default | What it does |
|---|---|---|
-seeds |
20 | how many seeds to run |
-first |
1 | the first seed of the range |
-seed |
0 (off) | replay this one seed instead of a range |
-steps |
10000 | the fault phase's step budget per seed |
-stopat |
0 (off) | end a replay at this step, printing the trace and a dump of the record and ground truth |
-quiet |
2000 | the quiet phase's bound: the steps the record has to settle in once faults stop; four times this on the file store, where every filesystem and lock call is a step |
-twice |
true | run each passing seed again and compare trace hashes |
-coverage |
false | fail the batch if any required event never happened in a shape |
-simtrace |
false | print every seed's trace, not only a failing one's |
These are test flags, so they go after the package path. TestKnobs adds two more, -knob and
-knobseeds; see the knobs.
The simulator runs inside a testing/synctest bubble on a fake clock, so a seed that simulates
hours of scheduler time takes milliseconds of real time. Large batches still use memory: cap them
on a shared machine, for example
systemd-run --user --scope -p MemoryMax=4G env GOMEMLIMIT=3GiB go test ./internal/sim ....
Read a failure¶
A failing seed stops the batch. The last lines give the seed's configuration, the invariant it
broke, and the command that replays it. This one is real: seed 1 with the RetireCanonical knob
turned on, so a fix is deliberately missing (see the knobs):
seed 1 shape slurm sups 3 unmatched residual faults {lostReply:true lateLanding:true failBefore:true lostCommit:true crash:false stop:false revoke:true powerCut:false omit:true}: 1502 steps
invariant 6: retired handle 1 was opened
replay: go test ./internal/sim -run TestSim -seed=1
Above that, the test log holds the trace, one line per scheduler step, then a dump of the record and the scheduler's ground truth at the failure:
1473 00:09:24.651 sup2#0 Find
1474 00:09:24.651 sup2#0 Commit retire
1475 00:09:24.651 sup1#0 fs open .tmp-d9fecc14e2ed9629
1476 00:09:24.651 sup1#0 fs sync .tmp-d9fecc14e2ed9629
1477 00:09:24.651 sup2#0 flock tx.lock
1478 00:09:24.732 sup0#0 Commit retire
...
1482 00:09:24.732 sup1#0 fs rename record
record rev 35 desired 1 outcome &{State:failed ...} settled false holds 4 requeue false
sub 1 key initial disp 1 canonical &{memory 1 2000-01-01 00:01:25 +0000 UTC} attempts: [1 reply 3 matched false abandoned false] [2 reply 1 matched true abandoned false]
obj 1 n1 a2 retired true ended false vstop false seen true foreign false disposed false last held
hold 4 kind backend active true refused false until 01:05:06
escalation give-up: sim
truth 1 n1 a2 held opened true hold 4 purged false foreign false
Each trace line is the step number, the fake time, the process and its incarnation (sup0#1
would be the first supervisor after one restart), and what it did. A deliver line is a call the
fault layer held back and landed later. On a seed that runs the file store, as this one does, every
filesystem and lock call is a step too (fs, flock). In the dump, obj lines are what the record
says about each object and truth lines are what the memory adapter knows: here the record has
object 1 retired while the scheduler shows it opened.
Replay a seed¶
The replay prints the same trace, because the run is deterministic: same seed, same
configuration, same order of every call. Add -simtrace to see the trace of a seed that passes.
The replay command a failing TestSim prints doesn't carry a knob. To replay a seed a knob was
caught at, turn the same knob on, as the line TestKnobs logs for each caught knob does:
DISPATCH_MUTANT=RetireCanonical go test -tags dispatch_mutants ./internal/sim -run TestSim -seed=1 -v
If -twice reports "is not deterministic", the two runs of one seed diverged. It prints the lines
around the first difference. That's a bug in the simulated code or the simulator, usually a map
ranged without sorting, a draw from a shared random source, or a goroutine started on the
simulated path.
Gate on coverage¶
With -coverage, the batch fails for any required event that never happened in a shape, naming
it:
A fault that never fired is a gap in the simulator, not a pass. A small batch is expected to miss
rare events. The required lists are in internal/sim/coverage.go: coverageBoth, coverageSlurm
and coverageKubeflow per shape, and coverageFile once over the seeds that ran the file store,
which fails as coverage: tombstoned never happened on the file store (spec 0005 D8).
Check the mutation knobs¶
Each knob turns one fix back off, one at a time, and the simulator must then find a violation
within the seed budget (-knobseeds, default 500, starting at -first). A caught knob is logged
with the seed and what broke:
knobs_test.go:56: knob RetireCanonical caught at seed 1: invariant 6: retired handle 1 was opened
replay: DISPATCH_MUTANT=RetireCanonical go test -tags dispatch_mutants ./internal/sim -run TestSim -seed=1
Run one knob with -knob=<name>. The names are in internal/mutants/off.go. The knobs only exist
in a build tagged dispatch_mutants; in every other build they're constants the compiler removes.
A knob not caught fails the test, unless the maintainer has accepted a written defence-in-depth
argument for it. Those are in acceptedExempt in internal/sim/knobs_test.go; an accepted knob
that isn't caught passes, and the log quotes its argument. Two are accepted, provisionally, as of
2026-10-12: SingleCancelSweep and VerifiedStoppedAsEvidence.
What CI runs¶
| Job | When | Command |
|---|---|---|
simulator |
every merge request | 500 seeds with -coverage |
simulator-knobs |
every merge request | TestKnobs with the default budget |
simulator-nightly |
scheduled, in 10 shards | 5,000 seeds per shard with -coverage and -twice=false |
simulator-race |
scheduled | 500 seeds under -race |
The commands are in .gitlab-ci.yml.