jonnify·bcs /bcs/bus/state-machine

B3.3 · The State Machine in Lua — manuscript chapter 3.3

Four states, four transitions, two pumps — every one a single script.

The bus learns to move work through states: pending, active, scheduled, dead. The rung gates the surface delta, the happy path, a fenced zombie, a job's second life through the schedule, the morgue, and crash recovery by reap — and the design's quiet center is one integer doing two jobs: the attempts counter, incremented inside the claim script, is also the fencing token every other transition verifies.

The chapter is content/bcs3.3.md; the rung behind it is bcs_rung_3_3_check.exs, and its committed transcript closes PASS 6/6. The surface, the happy path, the fence, the schedule, the morgue, the reap — six gates, each asserted on stage.

The machine, drawn; the six gates, mapped to the dives. Four states and the transitions between them. Select a gate to light the path it exercises and read its verbatim line.
claim · mints the token reap · lease expired complete · right token retry · parks promote · the due back retry at max pending score zero · lex = mint order active scored by lease deadline ∅ nothing remains schedule scored by run-at dead score zero · the morgue
readout · Select a gate to light the states and transitions it exercises and read its verbatim line. Six gates over four states; the full transcript is frozen in §2 below, the dives are mapped in §3.
§1

Fenced from the first transition

content/bcs3.3.md · Why · Decisions

A queue that hands out work without leases leaks jobs the first time a consumer dies; a queue with leases but no fencing corrupts data the first time a consumer pauses — the classic failure where a process stalls past its lease, the job is handed to another worker, and then the first wakes up and completes stale work over fresh state. Kleppmann's analysis named the cure — a lock service that "generates strictly monotonically increasing tokens", with the resource refusing any token that goes backwards. On a trading desk the stakes are not abstract — a paused consumer finishing a stale fill is a double execution — so the machine is built fenced from its first transition, not patched fenced after its first incident.

THE CLOCK

The server clock owns leases

Clients never send now; TIME inside the script is the one clock, skew-immune by construction, with mint time staying history inside the id — the two-clocks law, applied to the bus exactly as the preface promised.

THE TOKEN

attempts is the fencing token

One counter, two duties: it counts lives and it fences them, monotonic because only the claim script increments it. A second token field would be a second authority.

THE EXCEPTION

One constructed key, sanctioned by grammar

Claim builds the job key from a prefix because the id is unknown until popped; every other script declares its keys in full. The exception is safe because of the co-location law and exists only under it.

THE WIRE CLASS

EMQSTALE joins EMQKIND

The wire-class family grows by one; every refusal a bundle script issues leads with its class.

DELETION

Completion deletes

A successful job leaves no residue by default — the row is gone, the receipt is the caller's reply. The audit and event trail is the lane the manuscript assigns to B3.5 · Bus Meets Stores, pre-stated here so deletion reads as a decision rather than an omission.

§2

The proof

content/bcs3.3.md · quoting bcs_rung_3_3_check.out

The full committed transcript, verbatim — six gates, L1 through L6:

content/echo_data/runtimes/elixir/bcs_rung_3_3_check.out · verbatimfrozen
L1 surface ok -- the machine's surface: claim, complete, retry, promote, reap join enqueue, browse, pending_size -- five new verbs, every transition one script
L2 happy ok -- claim hands out the oldest job with a server-clock lease and fencing token 1; complete with the right token retires the row -- nothing remains
L3 fence ok -- a stale token is refused on the wire: EMQSTALE; the lease holder's work survives the zombie's complete
L4 schedule ok -- retry parks the job in the schedule, promote moves the due back to pending, and the next claim hands token 2 -- one job, two lives, one counter
L5 dead ok -- attempts 2 against max 2 is the morgue: state dead, last_error kept, and the dead set browses in mint order like everything else
L6 reap ok -- a 40 ms lease expires unanswered; reap returns the orphan to pending and the next claim holds token 2 -- crash recovery is one zset scan on the server's clock
PASS 6/6

L1 fixes the surface delta: claim, complete, retry, promote, reap join enqueue, browse, pending_size — five new verbs, every transition one script, the surface gated at exactly the eight verbs. The bundle lives inside runtimes/elixir/lib/echo_mq/jobs.ex — five Script.new constants, SHA-pinned, dispatched EVALSHA-first through the appendix's loader.

§3

The dives

three lessons, fixed by the chapter spec

Both pumps — reap and promote — need a driver, and the loop that owns the rhythm is the lane the manuscript assigns to B3.4 · Fair Lanes; this module owns the correctness of each beat. The pending set's score-zero law arrives from B3.2 · Jobs Are Entities, honored here.

§4

References

sources · related

Sources

Related