jonnify·bcs /bcs/cache/single-writer-ring/the-storm-drill

B4.3 · dive 3 — the storm drill

A fill fired mid-storm completed in 0 ms.

Five hundred invalidations published on the real wire — push frames into the owner, which now only parses and publishes; the ring's applier does the applying. The committed gate: 500 invalidations crossed the wire and the ring in 25 ms with nothing dropped; every stormed row left L1, the one rewritten name answers px=109.00 from the shared L2, and a fill fired mid-storm completed in 0 ms — the owner parses and publishes while the applier applies, and neither waits for the other. That zero is the chapter's reason to exist. The race underneath is gated adversarially: 500 shuffled messages converging on exactly 100 applied and 400 stale.

Source: content/bcs4.3.md, quoting bcs_rung_4_3_check.out; the modules are committed at runtimes/elixir/lib/echo_store/ring.ex and the grown lib/echo_store/table.ex.

The decoupled owner. The Why's coupled counterfactual against the committed shape, both from the chapter; the storm's numbers are the record's own. Select a shape.
applies inline 500 applies on the owner — the fill at the end of the queue parse-and-publish + ring the applier drains the storm; the fill answers beside it — 0 ms
readout · Select a shape. The counterfactual is the chapter's own Why; the committed shape is gate G5, frozen in §1 below.
§1

The transcript

bcs_rung_4_3_check.out · verbatim · this dive reads the storm and convergence lines
content/echo_data/runtimes/elixir/bcs_rung_4_3_check.out · the slice this dive readsfrozen
…
derive (storm): 500 invalidations published on the wire ride push frames at the committed 72 us median into the owner, which only parses and publishes -- application happens on the ring's applier, so a fetch fired mid-storm answers without queueing behind 500 applies; expect the storm applied within two seconds and the mid-storm fill well under 50 ms
G5 storm ok -- 500 invalidations crossed the wire and the ring in 25 ms with nothing dropped; every stormed row left L1, the one name whose writer placed a new value answers px=109.00 from the shared L2, and a fill fired mid-storm completed in 0 ms -- the owner parses and publishes while the applier applies, and neither waits for the other
derive (convergence): for each of 200 names holding version v2, a shuffled stream delivers either v1,v3,v1 or v1,v1 -- whatever the arrival order, a row is dropped if and only if a version newer than v2 appeared, and the per-name verdict counts are invariant under permutation: exactly 100 applied and 400 stale
G6 convergence ok -- 500 shuffled messages converged: the 100 names that saw a newer version lost their rows, the 100 that saw only older versions still answer :hit, and the verdict counters landed exactly on 100 applied and 400 stale -- arrival order changed nothing, because every application is the same comparison
PASS 6/6

The elided lines hold the header, the surface and order drills, and the occupancy and full drills — dive 1 and dive 2 read them, and the module hub freezes the record whole. The derive lines are part of the record: each measurement is bounded before it is taken, and the committed line then lands inside its own band.

§2

The storm, with the owner decoupled

content/bcs4.3.md · Why · What · Where

In 4.2 the table's owner did two jobs: it served fills and puts, and it applied every coherence push inline. At 72 microseconds a message that coupling is invisible — until the storm: a halted market segment, a bulk reprice, a reconnect replay, thousands of invalidations in one burst. An owner that applies inline queues its fills behind them; an owner that spawns per message buys unbounded process churn for work that is one ETS comparison each.

The drill runs the storm on the real wire: "500 invalidations published on the wire ride push frames at the committed 72 us median into the owner, which only parses and publishes -- application happens on the ring's applier, so a fetch fired mid-storm answers without queueing behind 500 applies". The derive line bounds the outcome — "expect the storm applied within two seconds and the mid-storm fill well under 50 ms" — and the committed line lands far inside it: 25 ms for the whole storm, nothing dropped, every stormed row out of L1, the one rewritten name answering px=109.00 from the shared L2, and the mid-storm fill at 0 ms.

The wiring is the chapter's Where: the push handler reduced to parse-and-publish, the :broadcast init starting the ring before the subscription that feeds it, the spec carrying ring and ring_capacity, the terminate stopping the ring with the table.

§3

Convergence is order-independent

content/bcs4.3.md · What · Who · quoting G6

The applier races the owner's fills with no coordination, and the proof that this is safe is 4.2's own theorem run adversarially: for each of 200 names holding version v2, a shuffled stream delivers either v1,v3,v1 or v1,v1 — "whatever the arrival order, a row is dropped if and only if a version newer than v2 appeared, and the per-name verdict counts are invariant under permutation: exactly 100 applied and 400 stale". The committed gate: 500 shuffled messages converged — the 100 names where a newer version arrived lost their rows, the 100 where only older versions arrived still answer :hit, and "the verdict counters landed exactly on 100 applied and 400 stale -- arrival order changed nothing, because every application is the same comparison".

The ring preserves arrival order as a courtesy to observability, not as a correctness requirement. And the lanes stay separate: the broadcast lane rides receipt-to-ring-to-applier end to end, while the job lane deliberately does not — its consumer still applies through the owner's apply_coherence, because a lane sold as at-least-once cannot pass through a structure licensed to drop. The table's owner returns to its 4.1 job description: fills, puts, and nothing else.

The convergence comparator. The G6 dataset — 200 names at v2; 100 names receive v1,v3,v1 and 100 receive v1,v1, 500 messages — shuffled by fixed deterministic permutations and applied through the one comparison. The verdict bars are drawn from the committed counts.
applied · 100 a version newer than v2 — the row leaves, the verdict counts stale · 400 older than the row's v2 — refused by the same comparison
readout · Run the 500-message stream in any of the fixed orders. Every run applies the same comparison per name and counts the verdicts; the derive line names the invariant, G6 commits it.

The module hands forward: B4.4 · The Lane That Remembers — the journal writer is the next single-writer candidate; one owner draining ordered work is about to become a habit.

§4

References

sources · related

Sources

Related