12 KiB
ECP-0157: Rust Simulation Testing
Status: Draft
Context
Production is now fast enough to expose distributed bugs quickly, but it is still the wrong first place to discover scheduler, archive, and duplicate-publisher invariants. The Python node-agent also made this worse by putting core control behavior outside the already-built Rust node binary.
Decision
Add a small deterministic simulation layer in ec-core and use it for distributed media invariants:
ec-noderemains the runtime owner for node behavior.- Tests model logical time, delayed delivery, backfill, duplicate publishers, and archive convergence in Rust.
- Simulation scenarios are seed-replayable and include deterministic jitter, transient drops, partition windows, publisher outage/restart windows, backfill retries, and encoder drift faults.
- A failing simulation must print or carry a replay hint so the exact schedule can be rerun.
- Simulation reports include deterministic execution history so a failure has an ordered event trace, not only a final assertion.
- Simulation campaigns run many seed schedules in one fast test and preserve the first failing seed, invariant report, and final state as the failure artifact.
- Campaign execution has a reusable seeded runner so new models can share replay/failure accounting instead of copying bespoke loops.
- First failures are automatically shrunk where the model supports it. For duplicate publishers the shrinker removes irrelevant partitions, publisher outages, timing jitter, transient drops, and excess media sequence range while keeping the original invariant unchanged.
- Invariants are explicit checks, not implicit test prose: duplicate source count, missing sequences, divergent hashes, missing media timing, conflicting media timing, complete duplicate coverage, and convergence-deadline budgets.
- Media identity is checked by BLAKE3 hashes for stream, rendition, track, sequence, profile, and source-material identity.
- Media timing is part of the proof model. Matching hashes are not considered a complete duplicate proof unless both publishers also expose a shared logical media clock for the chunk.
- Source-material identity is separate from stream metadata. Two publishers can advertise the same channel, sequence, timing, and encoder profile while still encoding different RF/source windows; that must fail in simulation before production archive comparisons burn wall-clock time.
- Publisher-origin archive
group_sequenceis derived from parsed media-time identity plus stable track id, not local receive time. Receive time is telemetry; it is not proof that two publishers archived the same broadcast moment. - Live publisher archive proof normalizes fMP4
tfdtto the Unix media slot before hashing a fragment. The first fragment for each track anchors the process-local media clock to wall-clock time; later fragments preserve ffmpeg's media cadence from that origin. ffmpeg still runs with wall-clock timestamp input enabled where possible, but the Rust archive writer is the authority for the proof clock when source MPEG-TS timestamps are process-relative. - Archive
group_sequenceincludes a stable subfragment slot inside each(track_id, media_sequence)pair, because audio can legitimately emit multiple fragments within one media slot and those must compare in order instead of colliding as source-local divergences. - Duplicate-publisher scenarios model publisher content phase separately from advertised archive sequence. A publisher that starts its local encoder at a different content phase must fail fast in simulation, because production fragments with the same local sequence are not proof of the same broadcast moment unless the chunk clock is shared.
ec-node sim-duplicate-publishersruns the same campaign model from the compiled Rust binary and emits JSON suitable for CI artifacts and rollout gates.ec-node sim-duplicate-publishers --failure-artifact <path>writes the first failing campaign as a replayable JSON artifact with the shrunk scenario, invariant report, event trace, shrink steps, and a command hint for replayingreplay_scenariothrough--scenario-json -.ec-node sim-duplicate-publishers --scenario-json <path-or->replays an exact serializedDuplicatePublisherScenario, so a shrunk failure from CI or production investigation can be rerun without reconstructing command-line flags.ec-node sim-duplicate-publisherscan inject timing faults directly with--missing-media-timing-publisher NODEand--publisher-media-time-offset NODE:OFFSET_MS, so the current production proof class can be reproduced without hand-writing scenario JSON.ec-node sim-duplicate-publishersandec-node sim-systemcan inject source-window faults with--publisher-source-material NODE:MATERIAL_ID. Any campaign with multiple source-material ids reports source-material mismatch observations instead of leaving operators to infer that class from divergent hashes.ec-node archive-convergencereads existing archive manifest JSONL and applies the same convergence semantics to real duplicate publisher outputs.- Control-plane simulation models logical nodes, seeded gossip fanout, delivery jitter, transient drops, node-specific partitions, node outages, duplicate deliveries, and propagation deadlines.
ec-node sim-control-planeruns the control-plane model from the compiled Rust binary and emits replayable JSON with the first failing seed, scenario, invariant report, and ordered trace.- Control-plane campaign reports track max propagation time, max delivery time, dropped messages, partition-delayed messages, outage-delayed messages, and duplicate messages, so prod rollout measurements have a fast simulation baseline.
- System simulation composes control-plane propagation with duplicate-publisher media production. Control gossip produces per-publisher activation times; the media workload then proves that delayed schedule propagation still converges when publishers use the global media sequence clock and fails when they derive chunk identity from local activation time.
ec-node sim-systemruns that composed workload from the deployed node binary. Its default campaign models the current publisher topology class and can switch--sequence-clockbetweenglobalandlocal-activationto reproduce the exact class of duplicate-publisher phase bug before waiting for production samples.ec-node sim-system --fault-profile foundationdbuses a FoundationDB-style fault profile: each seed generates a different but replayable cluster schedule with randomized control partitions, node outages, transient gossip drops, duplicate messages, media partitions, publisher outages, and archive backfill pressure.- The FoundationDB-style profile must also have an explicit negative regression for
local-activationsequence clocks, so the model proves the current production failure class is caught in Rust before any rollout waits for live fragments. ec-node sim-system --failure-artifact <path>writes the first failing composed system schedule as replayable JSON, including the exact control/media scenario, invariant report, ordered trace, and command hint for rerunning--scenario-json -.- System campaign reports must include fault coverage counters, not just pass/fail. A fast campaign is only useful if it proves that the simulated run actually exercised the failure modes operators care about.
- System campaign reports also aggregate publisher phase-offset observations. A production-like divergence caused by local activation clocks should identify itself as a phase bug in the campaign JSON instead of requiring operators to infer that only from divergent hashes.
- System campaign reports also aggregate source-material mismatch observations. A production-like divergence caused by independent tuner/source windows should identify itself as a source-material bug in the campaign JSON instead of being confused with codec nondeterminism.
- System and duplicate-publisher reports aggregate missing media-timing records and media-timing conflicts, so the live failure class where fragments arrive without a usable media clock is visible in fast Rust simulation output.
- FoundationDB-profile
sim-systemcampaigns require that coverage by default: control transient drops, partition delays, node outage delays, duplicate messages, media transient drops, media partition delays, publisher outages, backfill, and observed convergence timing must all appear in the campaign report. A campaign that passes invariants but misses these classes is reported as a weak simulation, not a green rollout gate. - FoundationDB-profile coverage is breadth-gated, not only boolean-gated. By default at least
max(2, iterations / 32)seeds must exercise every required distributed fault class; operators can raise that floor with--min-fault-seed-coveragefor longer scientific campaigns. - Campaign reports track both event totals and seed counts per fault class, plus a bounded list of the slowest system schedules with replay hints. This makes green runs inspectable: operators can see how broadly the randomized schedule space was exercised and which seeds define the current latency tail.
- System campaign reports also aggregate deterministic simulated convergence time and trace event
counts.
ec-node sim-systemstamps wall-clock execution telemetry around the campaign so a run reports iterations per second, simulated system seconds per wall second, and trace events per second without putting wall-clock data into the replayed scenario itself. sim-system --failure-artifact <path>writes an artifact for weak coverage as well as invariant failures, so CI can preserve evidence when a campaign was too small or too narrow to exercise the required distributed faults.- Forge
ci-gatesruns the Rust system simulator tests and a 1024-seedsim-system --fault-profile foundationdbcampaign from the compiledec-nodebinary before web build/deploy gates. This keeps the fast randomized check ahead of production rollout evidence. - Simulation failures must be actionable before any matching production rollout is considered healthy.
Consequences
We get FoundationDB-style pressure in a much smaller shape: many deterministic failure schedules can run as normal Rust tests without booting machines. The first media model covers duplicate publisher convergence, network partitions, transient loss, publisher restart/backfill, convergence latency, encoder drift, and publisher phase alignment, and the first runtime command applies it to archive manifests. The first control model covers gossip propagation across relays and nodes under dropped, delayed, duplicated, partitioned, and outage-delayed control messages. The shrink/replay path makes supported failures small enough to debug before they become production event archaeology; exact scenario JSON is the replay contract. Later models can add tuner scheduling, relay cache eviction, and image rollout state machines. The composed system model is the first workload-level step: it checks the boundary between control-plane speed and media determinism, which is where production duplicate publishers are currently most fragile.
Alternatives considered
- Keep writing production probes only. Rejected because probes prove what happened once, not what should happen across many fault schedules.
- Extend the Python node-agent as the simulation oracle. Rejected because the image should get thinner and the runtime behavior belongs in the Rust node.
Rollout/teardown
Roll forward by adding simulation tests next to each new distributed invariant. Roll back by keeping the production probes; the simulation module is library-only and has no runtime service impact.