jonnify·bcs /bcs/cache/coherence-by-mint-time

B4.2 · Coherence by Mint Time — manuscript chapter 4.2

Twenty-nine bytes, two lanes, and no clock but the name.

Chapter 4.1 bounded staleness by the clock; this chapter bounds it by the write. The coherence message of EchoStore is twenty-nine bytes — a cached name, a colon, and the writer's mint-time version — and everything else falls out of the order theorem: because a branded id's payload bytes sort in mint order, newer wins is a comparison of two names, with no coordinator, no lock, and no clock but the one already inside every id. Two lanes carry the same message, and the committed record prices them on one row: broadcast median 72 us fire-and-forget, job lane median 148 us at-least-once -- the guarantee costs 2.1 times the latency.

The chapter is content/bcs4.2.md; the rung behind it is bcs_rung_4_2_check.exs, and its committed transcript closes PASS 6/6. The vocabulary, newer-wins, the broadcast band, the loss, the crash, the price — six gates, each asserted on stage.

One message, two lanes; the six gates, mapped to the dives. The writer puts the value in L2 and sends the twenty-nine bytes down a lane; receivers drop, never write. Select a gate to light the path it exercises and read its verbatim line.
PUBLISH · 29 bytes enqueue · 29 bytes put · px=106.00 + version push · drop if newer claim · lease + token the next read falls through writer mints the TXN version ecc:{quotes}:coh the broadcast lane · at-most-once ecc.coh.qc the job lane · at-least-once L2 · Valkey the shared layer the writer updated L1 owners subscribed tables · one comparison consumer coherence_handler · supervised
readout · Select a gate to light the nodes and lanes it exercises and read its verbatim line. Six gates over one message and two lanes; the full transcript is frozen in §2 below, the dives are mapped in §3.
§1

A message that carries its own order

content/bcs4.2.md · Why · Decisions

A TTL is a promise about the past — this row was true within the last N milliseconds — and for quotes on a screen that promise is enough. For a position limit, a halted instrument, or a risk parameter, it is not: the desk needs this row reflects the latest write, and the latest write is exactly what the comparison set cannot say. Valkey's own tracking sends an unversioned forget this key; Nebulex synchronizes by deletion; neither message carries an order, so a late invalidation and a fresh write race, and the loser is whoever applied last. The fix the literature reached in 1978 is a total order constructed from timestamps, and this series has carried that order in every identity since Part I. The manuscript plans the measured face-off — B4.5 · The Cache Referee — and until that chapter ships, the comparison set is characterized, never measured.

The writer's obligation is one call after the store write — the coherence: slot declared by B4.1 · Cache-Aside at ETS Speed's table directory, now wired:

version = BrandedId.generate!("TXN")           # the write's own identity
:ok = Table.put(:quotes, ast_id, "px=106.00", version)
{:ok, _heard} = Coherence.broadcast(conn, "quotes", ast_id, version)
# or, when at-least-once matters:
{:ok, :enqueued} = Coherence.enqueue(conn, "quotes", group, ast_id, version)
THE VERSION

The version is a name

Not a counter, not a wall-clock reading, not a vector: a branded id whose payload bytes already total-order by mint time across every kind. One identity system, one more dividend.

THE DROP

Coherence drops; it never writes

A message means the writer already placed the newer value in L2; the receiver's only move is to discard its older L1 copy. The lane carries names, not rows.

BOTH LAYERS

Stale messages bounce twice

The L1 comparison in the owner and the L2 comparison in one Lua script are the same predicate in two places, because a late invalidation that can erase a newer row is a coordination bug wearing a latency costume.

THE TREE

The consumer lives in the application's tree

An EchoMQ consumer is a real supervised worker with a lease and a lane; hiding one inside a cache table would falsify the supervision tree. coherence_handler/1 makes the wiring one line; the tree stays true.

THE PUSH PATH

Send-only, RESP3-only

A SUBSCRIBE confirmation arrives as a push, so awaiting it on the FIFO starves the queue — the new verb sends without enqueueing an expectation, and refuses protocol 2 with a typed :requires_resp3.

THE APPLIER

The owner — for now

Pushes apply inline in the table's process, which is correct and measured fast; B4.3 · The Single Writer and the Ring moves application onto the ring for batching and backpressure without touching the semantics gated here.

§2

The proof

content/bcs4.2.md · quoting bcs_rung_4_2_check.out

The full committed transcript, verbatim — six gates, F1 through F6, the derive lines and the staged consumer kill included. The mid-record [error] GenServer … terminating block is the F5 drill's own kill — part of the frozen record, not noise around it:

content/echo_data/runtimes/elixir/bcs_rung_4_2_check.out · verbatimfrozen
F1 surface ok -- the vocabulary is whole: channel, queue, a twenty-nine-byte payload of two names, parse refusing garbage; tables declare their lane in the directory; and the connector's push path refuses a protocol 2 connection with a typed :requires_resp3
F2 newer-wins ok -- a late stale invalidation bounced off both layers -- the L1 row survived holding px=105.00 and the L2 drop script answered 0 -- while a genuinely newer version applied and the replay of the old one stayed stale: idempotence is a comparison, not a log
derive (broadcast): the lane is one PUBLISH hop on the wire whose committed sequential floor is 29,456 round trips per second, near 34 us each -- expect a median push latency between 30 and 500 us, and the receiver's apply is one ETS comparison on top
F3 broadcast ok -- median push latency 72 us over 100 messages, inside the derived band; the cross-node round trip holds -- the writer put px=106.00, 3 subscribers heard the name, and the other node's next read fell through its dropped L1 to the shared L2 and answered fresh
F4 loss ok -- the price of fire-and-forget, stated as a gate: :qc declared the job lane and holds no subscription, so the broadcast passed it by and it still serves px=100.00 -- bounded staleness until its own lane delivers, which is the next gate's business
derive (job lane): a consumer crash after claim strands the coherence job on a lease; the reaper returns it, a second consumer applies it with token 2, and reapplication is harmless because newer-wins is a comparison -- expect attempts 2 and exactly one effective drop

01:36:22.839 [error] GenServer #PID<0.203.0> terminating
** (stop) killed
Last message: {:EXIT, #PID<0.202.0>, :killed}
State: %{backoff: 100, buf: "", client_name: nil, connect_timeout: 3000, counters: {:write_concurrency, #Reference<0.595650664.2344747009.170073>}, database: 0, hb_ref: #Reference<0.595650664.2344615937.170091>, heartbeat_ms: 30000, host: {127, 0, 0, 1}, label: :emq, max_pending: 10000, overloads: 0, password: nil, pending: {[], []}, pending_n: 0, port: 6390, protocol: :auto, protocol_live: 3, push_to: nil, pushes: 0, sock: #Port<0.13>}
F5 job lane ok -- the lane that survives: the first consumer died holding the job, the reaper returned it, the healer applied it -- :qc dropped its stale row and now serves px=107.00 from the shared L2 -- the completed job left no row to browse, and replaying the same version answers stale: at-least-once delivery, exactly-once effect
derive (price): the broadcast lane is one wire hop; the job lane pays three to five hops -- enqueue, wake, claim, complete -- so a parked consumer should land its median between 80 us and 2 ms, the same order as the bus's committed 0.3 ms end-to-end median, carrying the guarantee the push cannot
F6 price ok -- the two lanes on one row: broadcast median 72 us fire-and-forget, job lane median 148 us at-least-once -- the guarantee costs 2.1 times the latency, and gates F4 and F5 are the reason a surface pays it
PASS 6/6

The derive lines are part of the record: each measurement is preceded by the band the rung derived for it from already-committed figures — the wire's sequential floor of 29,456 round trips per second prices the PUBLISH hop, the bus's 0.3 ms end-to-end median prices the job lane's three to five hops — and F3 and F6 land inside their bands. One production module lands (runtimes/elixir/lib/echo_store/coherence.ex), the table grows version-aware (lib/echo_store/table.ex), and the connector gains the send-only push path (lib/echo_mq/connector.ex).

§3

The dives

three lessons, fixed by the chapter spec

The lane vocabulary is channel names, queue names, and a twenty-nine-byte frame — nothing in it assumes the BEAM, which is what the Go and Node ports will hold it to. The application engine under both lanes is the next module's business: B4.3 · The Single Writer and the Ring inherits the applier the rung gated here.

§4

References

sources · related

Sources

Related