Repository navigation
fix(sim): stop a run when its simulated clock stalls, not when it is slow - #786
Conversation
vorstand-edna-bot
left a comment
There was a problem hiding this comment.
Approve: the stop is a wall-clock decision on a side thread that only cuts a run off and labels it; outcomes per seed stay deterministic, a stalled clock is caught within STALL, a clock that keeps moving is caught at CEILING, and the campaign's RUN_LIMIT remains the backstop.
Blocking
None.
Non-blocking
crates/dassie-sim/src/campaign.rsWatch::look: "how far the run got" counts from the first clock the watch sampled, about one wall second after the process started, not from the simulation's start. For runs where a simulated day takes seconds (stubnode,peer), that first sample can already be hours in, so "stood still at 12 s into the run" can understate by a lot. It only affects the message. If you want it exact,dassie_kit::simcould keep the first published value in a second atomic (set once, by compare-exchange from 0).
…slow Runs of the node simulation over two settlement hubs take tens of minutes of wall time on a campaign's loaded VM while their clocks move on, so the 10-minute limit reported sound runs as stuck (#307). Every simulation now publishes its simulated clock, and a run's own process watches it: a clock that stands still for STALL is a liveness failure at `stalled`, a run still moving at CEILING one at `wall-time`, and both records say how far the run got. The campaign stops a process that writes neither a minute past the ceiling, and the supervisor gives the campaign's step that much beyond its minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75fc103 to
ad8907e
Compare
vorstand-edna-bot
left a comment
There was a problem hiding this comment.
Approve: this head only rebases the commit I approved; its patch is identical, and it still fits main.
The sim PRs merged on main since then (#772, #774, #778, #780, #781, #785) change hub-run scripts and checks, not the loops that move a clock. Every place a simulation moves its clock is still the same loop, and that loop publishes it: the node loop (run_until_or), the peer loop (run_until), the hub loop, stream and connector. The simulation list is unchanged, so no new simulation or runner path skips the watch. The supervisor and the runner's campaign code did not change on main. Merged with main as it is now, the spec edits in spec/areas/testing.md and spec/decisions/random-seed-simulations.md still read consistently, and the PR merges without conflicts.
Blocking
None.
Non-blocking
- As before: in
Watch::look, "how far the run got" counts from the first clock the watch sampled, about one wall second in, not from the simulation's start. For fast runs, this can make the reported figure much too low. It only affects the message.
Cause
RUN_LIMIT(10 minutes,crates/dassie-sim/src/campaign.rs) assumed the longest runs take a minute and a half alone. Since hub runs joined thenodeswarm, that no longer holds. Hub validators make a block every 100 ms, with real ed25519 signatures and real SQLite at every step. A simulated minute costs about 4 s of CPU for one hub with two members, and about 20 s for two hubs of seven validators with five members.Measured on hecate:
node 2449240600872231(two hubs, five members, a 941 s split, a 42 s heal) takes 302 s of CPU alone and passes.4870064036505252and737283724352507took 790 s and 878 s of wall time; alone they take 60–84 s.So on the 56-job campaign VM, sound runs crossed the limit. That is #307's ~32 hits per loop. The limit also hid what those runs would have found.
Change
dassie_kit::sim::publish_clock, a process-wideAtomicU64, is called by every simulation's clock loop: node, peer, hub, stream and connector.dassie-sim recordstarts a watch thread (campaign::watch), and a pure rule (Watch::look) decides when to stop:STALL(5 min): liveness failure atstalled.CEILING(90 min): liveness failure atwall-time.main.rs). Therecordcommand no longer holds stdout while the run runs.RUN_LIMITis nowCEILINGplus a minute. It still sayswall-time, now "without a record of how far it got".(minutes + RUN_LIMIT_MINUTES + 5) * 60, so a campaign's last runs end inside the step. A test reads the runner's constants fromcampaign.rs, so the two can't drift apart.spec/areas/testing.md, the runner, a range of seeds and the campaign: the new stopping rule, and a corrected cost of a simulated hour over hubs.spec/decisions/random-seed-simulations.md: a decision bullet, and the per-run wall-time limit as a rejected alternative.How the windows are derived
STALL: a sound run's clock stands still only while it handles one instant's events (a few hundred steps of milliseconds each) and while it judges the run after its last instant (one pass over its trace and books). That is seconds, even for the heaviest runs on a loaded VM. Five minutes is a hundred times that, and still catches a stuck run sooner than the old 10 minutes did.CEILING: the longest runs arenoderuns over two hubs. They can reach about two simulated hours: the chaos, a split over the hubs of up to 16 minutes, the network forming again, and a heal that may wait out the longest backoff, an hour. At ~20 s of CPU per simulated minute that is ~40 minutes alone, and about twice that next to another run on the core's other hyperthread. Hence 90 minutes.Evidence
a_run_whose_clock_moves_runs_up_to_the_ceilinga_run_whose_clock_stands_still_is_stopped_after_the_stall(including a run that never publishes a clock)a_stopped_run_says_why_and_how_far_it_got(both signatures and messages)the_campaign_waits_past_the_ceilingthe supervisor waits as long as the runner's run limitinscripts/sim-campaign_test.ts.record node 2449240600872231wrote onenode/liveness/wall-timerecord, "still running after 4 s of wall time, 15 s into its simulated time", and exited.record node 12345; a fewnodeandpeerranges).Checks
cargo fmt --check; clippy on dassie-sim, dassie-kit, dassie-hub, dassie-stream, dassie-connector, dassie-peer and dassie-node.node --test scripts/sim-campaign_test.ts,node --test scripts/spec_test.ts, dprint andtsc.Trade-offs
…/liveness/stalled, and…/liveness/wall-timenow means "too slow to end within the ceiling".Fixes #307
🤖 Generated with Claude Code