ADR-041: The block-epoch pipeline — one stage machine over a cheap-read data plane¶
Status: implemented (2026-09-07, ergo epic MROOY7 / task PLRGIN).
Settled in the originating architecture conversation, recorded here so every
later task can execute without it; the user checkpoint on this ADR was
sign-off of the stage table and seam retirement list (canonical form in
the design doc). Implemented across
the epic’s tasks and validated by the final-integration gate: capture-replay
regression sweep (zero divergences; see PLRGIN result) and the live Jaeger
soak A/B against the pre-epic operator baselines
(../architecture/stateview-feasibility.md §3). The unified StageMachine
lives at rust/crates/engine/degenbot-bot/src/bot_core/stage_machine.rs (ergo
7NFYQW); the retired DrainSink/Engine/SolveCoordinator/
DispatchOwner/DirtySets/EngineSubscriber seams are gone (SZJUKL).
Context¶
The per-block loop of the arb engine is currently choreographed by six correlated state machines plus a parallel accounting system built around them:
EnginePhase,BlockClock(ADR-008),PumpFSM(ADR-028), the registration verify-lifecycle (ADR-022), the path lifecycle, and the delivery lifecycle — living inrust/crates/engine/degenbot-bot/src/bot_core/{block_pump.rs,pump_fsm.rs,block_clock.rs}andrust/crates/engine/degenbot-bot/src/arb_engine/{mod.rs,lifecycle.rs,path_lifecycle.rs,delivery_lifecycle.rs}. Each is pure (the ADR-028 producer/driver family), but their interaction is opaque: which machine may advance in a given block, and in what order, has no single owner.A parallel accounting layer — the
DrainSink/Enginedual seam,SolveCoordinatorwith itsdrain_lock, theDispatchOwner+DrainWorkFIFO,DrainerHealth, theDirtySets+EngineSubscriberclassification, and a soup of anchors (solve/sim/verifier/backfill anchors,last_solved_block,last_drained_block,BlockMetadata) that each carry an answer to “what block is this work about?”.The engine
Mutexsits on the solve path (sharded by ADR-037 for contention, but still an ordering constraint:drain_lock → engine Mutex → BotState RwLock).
The state-accuracy tripwire (ADR-021) exists largely because nothing structurally prevents a desynced snapshot from reaching the solver. A design in which the data plane’s writers are stage-confined makes the in-process half of that concern unrepresentable rather than merely detected.
An ergo spike (KWKEVV) already settled the data-plane mechanism question
(see the stateview feasibility doc recorded by that spike; canonical copy on branch pi-fabric/stateview-spike).
This ADR records the pipeline architecture that spike unblocked.
Decision¶
1. One pipeline stage machine per block epoch¶
A single stage machine owns every per-block edge condition. The canonical stage sequence is:
Streaming → Quiesced → Resolved → Solved → Simulated → Gated → Published →
Finalized, plus Rewind{to_epoch} on reorg.
Rewind is a transition from any stage to a fresh epoch at an earlier block;
the epoch’s seq bumps and stale contexts fail fast. Full stage
responsibilities, data-plane posture, and absorbed sub-state are specified in
the design doc.
The six existing machines fold in as sub-state of the stage machine;
their pinned tests are the behavioral contract and are ported verbatim.
Runtime decisions are dispatched through a single StageHandlers seam —
the pure per-stage function set the runtime drives — completing the ADR-028
pure-producer/thin-driver pattern at pipeline scope: BlockPump becomes an
event+tick feeder and decision executor only. Its reorg-episode and resume
tracking (block_pump.rs) folds into Rewind handling, and its WS transport
watchdog machinery later moves to
degenbot-ingestion(epic task5WTYYQ).
2. The data plane survives as cheap-read (spike KWKEVV outcome)¶
Cheap-read (mechanism (a)) for every family: V2-family scalars, V3 tickmaps (Uniswap/Pancake/Sushi), and V4 tickmaps. No materialization, no COW. Concretely:
StateLock<RwLock<BotState>>stays the data plane. Writers are confined to the Streaming stage. Quiesced, Resolved, Solved, and Simulated hold read guards; with the engineMutexoff the solve path, those reads are uncontended by construction.Measured basis (full method and repro in the feasibility doc): 90th-percentile CL tickmap density is 2–4 ticks (registry max 1,536); a full tickmap clone is 40–50 ns at p90 density and 16–19 µs p99 at the registry max; journal-replay of a full 32-block rewind is ≤ 23 µs p99 even at 8 priors/block;
ArcCOW view construction is 30 ns — all ~10⁵ under the 2 ms view-construction leg. The cheap-read exception leg did not trigger: per-path solve p99 is 5 ms and the solve gate alone 1 ms (≪ the 100 ms threshold), and livestate_lock_waitp99 is ≤ 0.1 ms across 19.6 M acquisitions — measured on the still-contended, pre-stage-machine architecture. Since cheap-read only loses if both rule legs pass, cheap-read wins outright for all families.Sizing for
Rewind: therestore_before_blockbounds (≤ 23 µs p99 V3 at depth 32; 20–30 ns V2) mean no view machinery is needed to hit the reorg budgets. StateLock hold/wait histograms are retained as the budget verifier. One out-of-corpus caveat is recorded: whole-map clone goes superlinear above ~131k entries (≥ 16 MB working sets, allocator/TLB) — irrelevant at today’s registry max of 1,536 ticks/pool.
3. Seam retirement¶
The parallel accounting system is deleted, not wrapped (hard cutover, Q6). Exact list, with disposition:
DrainSink/Enginedual seam → oneStageHandlersseam. The pump’s executor fan-out (ADR-028 addendum (c)) becomes one trait; delivery and submission subscribe at the Published edge as sinks, not as a dispatch seam in front of the engine.DirtySets+EngineSubscriberclassification →EpochDelta. Log application records touched pools as a byproduct (the delta rides theBlockContext); affected-path derivation reads the delta directly behind a capture-replay parity gate.SolveCoordinator,drain_lock,DispatchOwner,DrainerHealthdissolved. They accounted for work the stage machine now owns; the no-progress watchdog obligation moves onto the machine’s watchdogs.Engine
Mutexoff the solve path. ADR-037’s sharding made it low-contention; stage confinement removes it as an ordering constraint entirely. Registration/FFI locking viaStateLockremains (a slow operator path, not the solve path).Anchor soup → a single
Epochcarried onBlockContext. Solve/sim/ verifier/backfill anchors,last_solved_block,last_drained_block, andBlockMetadataare replaced by exactly one block coordinate; “what block is this work about” has one answer.
4. ADR-021 repositioning¶
ADR-021’s posture (detect, classify, stop loudly, never heal) is retained and referenced, but its mechanism repositions: in-process desync detection (solver-state vs chain scalar comparison) retires — desync becomes unrepresentable once writers are stage-confined — while RPC-disagreement verification at the Published edge is retained (the chain/RPC truth can still diverge from us; that failure stays loud and means the provider diverged, not our bookkeeping).
5. The Q1–Q6 decision record¶
Q |
Decision |
|---|---|
Q1 |
A+D hybrid confirmed — one stage machine (A) driving a stage-confined cheap-read data plane (D) |
Q2 |
Incremental in-place throughout. A temporary internal A/B gate is permitted only inside the stage-machine swap task, and must be removed by that task’s completion |
Q3 |
StateView mechanism was gated on the spike outcome; spike |
Q4 |
Arb engine ships first. Multi-engine machinery is a non-goal; the design is guarded by a |
Q5 |
12-factor config parity (file + env, one typed |
Q6 |
Immediate cutover on each task’s completion — 0.6 is alpha, breaking changes permitted. ADR-010/011 Alembic gating and the 0.7 kill list are untouched by this epic |
6. Runtime lifecycle is an orthogonal axis¶
Pipeline stages answer “what has happened to this epoch’s block”. The
process-level runtime lifecycle — Boot → Subscribed → SnapshotLoaded →
Resumed — is a separate axis and never interleaves with the stage
machine’s per-epoch transitions. A resume starts a fresh stage machine at
the snapshot-seed epoch; the sweep-back of epoch bookkeeping is what
SnapshotLoaded exists to bound.
EnginePhase is remapped in full onto this axis: its role —
registration/snapshot/backfill ordering — is runtime-level, not per-epoch, so
it contributes no Solved/Simulated sub-state to the stage machine. The
existing enum (Created → Subscribed → SnapshotLoaded → (Backfilled) → Resumed) already carries exactly this shape.
Consequences¶
One decision surface at pipeline scope. Exactly one component emits drain, publish, finalize, notify, backfill, recover, and verify decisions; the six machines are its sub-state. The ADR-008/ADR-028 pinned tests are the contract and are ported verbatim.
Composition desync goes away structurally, so its detector is deleted. The ADR-021 in-process tripwire surface retires; upstream verification at the Published edge remains.
Cheap-read means near-zero new machinery. No view representations, no COW memory, no replay cost on the steady path; the
ReorgJournalalready in the pools crate boundsRewind.NoopStubEngineis the executable spec of hook completeness. It asserts stage order, epoch monotonicity, and rewind handling so a future second engine cannot discover a missing hook at integration time, and keeps theStageHandlerstrait honest from day one.Tracing carries
epochon every span/metric, so Jaeger/Grafana tell the migration’s regression story directly.Migration order mirrors the epic task graph (epoch types →
EpochDelta→ StateView plane →StageHandlers+ stub → unified machine → telemetry → seam retirement → ingestion crate → final integration + status flip).
Non-goals¶
No multi-engine machinery. One arb engine exists; no registry, no routing, no second-engine configuration surface. The
NoopStubEngineis a test-declared conformance harness — a test-landmine guard, never runtime-selectable.No new StateView representation. The spike’s cheap-read outcome retires the COW/journal-materialization mechanisms; their costs remain recorded as sizing input for
Rewindand out-of-corpus growth only.No auto-repair. ADR-021’s posture is unchanged: divergence and the desync class stop loudly; nothing self-heals state from chain reads.
No backwards-compatibility layer. Per the repository architecture and Q6, retired seams are deleted with a hard cutover; no legacy exports are maintained in parallel.
No drift into the 0.7 ADR-010/011 kill list. Alembic retention, the SQLAlchemy models, and
ensure_schema’s Alembic branch are out of scope for this epic entirely.