14 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mem-engine-seam-tx | Mem engine — tx overlay, `Store` surface, `TxHandle` (staged `*_tx`, commit-merge + wake-at-commit, drop = discard) | completed |
|
moderate | high | component | implementation |
|
Description
The seam task, and the wave's load-bearing artifact
(engine-mem.md §The tx overlay): the Store trait impl plus the tx
overlay's full discipline.
Storesurface: the trait methods (notify,stream,queue,outbox,try_lock,listen,schedule,unschedule,run_schedules,begin_tx,with_tx) implemented over the state core.queue(name, opts)/outbox(name)create/return handles whose opts ride the handle (queues are names, not config owners — the §3a stamp rules apply identically). Mechanism handles are core-owned trait objects; engine types never appear in signatures.- The tx overlay:
begin_tx()allocates a handle-private overlay. All eleven*_txmethods stage effects in it —enqueue_tx,publish_tx,publish_with_key_tx,notify_tx,save_offset_tx,get_job_tx,get_offset_tx,read_since_tx,read_from_consumer_tx,outbox_enqueue_tx,commit. Reads inside the tx merge the overlay over committed state (read- your-own-writes, no round-trips: the tx-read rows'get_job_tx/read_since_txbehaviors come free, but pin them). Opt-stamping of staged effects runs at enqueue time on the overlay (resolution identical to the no-handle paths, per ADR-020 §3:enqueue_txstamps plain derived defaults 300/3/5/none;outbox_enqueue_txthe 60/5/5 set). - Commit-merge + wake-at-commit: commit merges the overlay
atomically (single guarded merge) and fires the overlay's pending
wakes in the same operation — a tx-staged notify delivers nothing
until the merge (the discipline pg got wrong and fixed via
pg-fix-tx-wake; here structural, and pinned from birth by the suite's tx-commit-atomicity rows once integration wires them). - Drop = discard: dropping the handle without commit/rollback
discards the overlay (rollback explicit form too) — no-ghosts
trivially, uncommitted state never touched committed state.
with_txcomposition:Ok⇒ commit,Err/drop/panic ⇒ rollback (the N-5 panic posture applies; overlay discard must be panic-safe through the unwind's drop). - Numeric domains (ADR-023 §2): validation at the trait-impl entry, before any storage-layer effect — extents clamp empty, durations reject, boundaries total. Same entry-point rule as the other engines, realized engine-side.
Engine-internal unit tests pin the overlay discipline directly
(merge atomicity, wake-after-not-before, drop-discard, panic-through-
unwind) in the family's mechanism-test posture; the suite column's
cross-engine rows land in mem-engine-integration.
Acceptance Criteria
- The full
Store/TxHandletrait surfaces implemented over guarded state; no engine types in consumer signatures - Overlay staging → commit-merge → wake-at-commit pinned by unit tests (a tx-staged notify delivers nothing pre-commit, one wake post-commit; enqueue/publish commit-atomicity shapes)
- Drop-without-commit and explicit rollback leave zero residue;
with_txpanic rolls back through the unwind's drop - Tx reads see staged effects; other views see none pre-commit; post-rollback reads' subjects gone (no-ghosts for reads)
- Numeric-domain guards at entry (extents/durations/boundaries) pinned per ADR-023 §2
cargo check --target wasm32-unknown-unknown -p alkstore-memclean; workspace gates green
References
- docs/architecture/core-contract.md §The seam; §Cross-cutting contracts
- docs/architecture/decisions/007-transactional-seam.md; 014-outbox-tx-enqueue.md; 020-enqueue-opt-semantics-and-bridges.md §3; 021-tx-reads-and-value-shape-fixes.md; 023-fourth-review-round.md §2
- docs/architecture/engine-mem.md §The tx overlay (the spec of record)
- tasks/pg-fix-tx-wake.md (the failure shape the overlay makes structurally impossible)
- tasks/review-wave-5.md appendix (the suite rows whose mem wiring consumes this task)
Notes
Filled by implementation agent, 2026-10-10.
- The whole engine surface landed here, deliberately — this task
is wider than the SQLite/pg seam twins. Those seams rode
pre-ported/forked substrate ops and left the store's mechanism
methods as wiring stubs; the mem engine has no substrate layer, and
this task's description + acceptance criterion 1 pin "the full
Store/TxHandletrait surfaces implemented over guarded state" — a Rust trait impl is all-or-nothing per method. So the seam task realized the mechanisms' committed-state effect layer itself (queue machine, stream log/offsets, TTL registry, scheduler table+runner, notify bridge) and every mechanism handle, with the tx overlay as the discipline centerpiece. Consequence for the wave's mechanism tasks:mem-engine-{notify-listen, streams, queues, locks, scheduler-outbox}now find their mechanisms implemented — their work is the engine-side unit pins their acceptance criteria enumerate (the ADR-010/015/009/008-§7 properties, the mem-owned curve/parser/stats pins, the receiver close arms) plus any depth refinement, against this foundation — not first implementations. Recorded here so those agents don't re-derive the split. serde_json = "1"joined the manifest (both engine twins carry it): the trait signatures crossserde_json::Value, so implementing them requires naming the type. Not a driver dep; the ADR-024 §1 surface (core + tokio sync+time) is otherwise intact, and the wasm32-compile gate stays green with it.- Stage-time id/offset allocation — the pg-shaped story:
enqueue_tx/publish_txreturn the id/offset immediately, so ids and offsets allocate at stage time from engine-wide atomic allocators (State::allocate_job_id/allocate_event_offset). A rolled-back (or panicking) tx's allocation gaps out — legal: ids are the claim-order tiebreak (claims order by id ASC like the SQL engines' enqueue order) and offsets are immutable/never renumbered (gaps ordinary). Cross-handle concurrency: merges serialize; reads yieldoffset ASC, so an event's committed position is always its stage order (no isolation questions arise — an overlay is invisible to every other view until its commit). - Canonical guard order
schedules → queues → streams(locks and wakes depend on no other section; documented instate.rs). The commit-merge takes queues+streams in one scope; the scheduler tick takes schedules+queues (the fire-and-advance one guarded op — the ADR-009 §4 no-double-fire floor); the wake fire happens after the merges land (wake-at-commit is structural: the fire code cannot run first). - Merge mechanics:
merge_and_wakedrains the overlay into the committed sections and then fires staged notifies (stage order, duplicates ride — per-notify, never coalesced) plus one wake per staged event on the stream's engine-internal feed. The stream subscriber's wake channel is__alkstore_stream_wake:{stream}— reserved-prefixed, so consumerlisten/notifycan never attach to or fire into it (entry validation rejects the prefix), and the EventReceiver attaches before its stored-offset read (no commit can fall between; a buffered wake costs an empty re-poll, never a missed event — the SQLite bridge's trick, now thread-free). - notify's payload encoding run is the codec-typed entry check
only: the wake contract delivers
Wake { channel }alone (ADR-008 §3), so mem stores no notifications transport — the encoded bytes are dropped after theCodec-typed validation (sqLite's transport row has no mem analog).PayloadTooLargeis never produced at any size (the ADR-016 §5 non-occurrence arm, pinned by a 2 MiB test). - Lock semantics vs the SQLite substrate: mem pins renew-past-
expiry refused (
false) — the SQLite substrate'slock_renewwould revive a not-yet-purged lapsed row; the mem task text pins the refusal and the suite's rows read the same way. Same-owner re-try_lockgrants without refreshing the deadline (theINSERT OR IGNORE+ owner-match parity); release past expiry stays the holder's (true); a lapsed-then-re-acquired row's old-holder release refuses (false). Locks are not tx-staged (nolock_tx— contract verbatim). ScheduleRowcarries the pre-parsed interval + resolved stamps — not the raw spec/name pair. The name is the map key (upsert by name) andparse_every_intervalruns once at register; the fire path reads registered values, never the spec (the substrate re-parses per tick — mem's third-owner parser makes that unnecessary).schedule()encodes the payload once at register; each fire enqueues those bytes with the registered stamps and theScheduleOpts::{priority, expires}riding themselves, resolved against the fire instant (one clock read per tick).- Runner lease: owner is runner-unique (
scheduler-leader-{ instance}-{subsecond-nanos}— an instance counter instead ofprocess::id, wasm-clean); renew-before-tick with the loss return before any fire; the keep-awake sleep renews on a 2 s cadence (an idle store must not drift past the 10 s TTL); idle sleep 60 s; refused acquire at start =Err(LeadershipLost)(no runner exists without the lock); a clean stop releases the lease. Boundary waits ride realtime tokio time while stamps read the engine clock — the tick/catch-up/skip-forward pins run through the crate-internalrun_tick(now)with the frozen clock, deterministic without realtime waits. - Row predicates mirror the substrate's SQL comparison-for-
comparison (claim: pending-ready or lapsed-processing + budget
unspent + expiry unpassed; ack
claim_expires_at >= now, reclaim<, lock purge/renew boundary<=, sweepexpires_at <= now). The test-shaped pins caught one real defect class mid-flight: ack/ack_batch originally deleted before checking the predicate — a refused ack must leave the row exactly as it was (the substrate'sDELETE … WHEREchecks per row); fixed, and the uniform-lapse refusal tests now pin it. - Sweep scoping note for later tasks:
sweep_expiredcounts both the dead-letter moves and the retention deletions in its return (substrate parity:live_moved + retained). A row whoseexpires_athas already passed at enqueue is not claimable and not pre-claim-dead-letterable — it sits forsweep_expired(the pre-claim sweep only takes exhausted claimable-shaped rows). enqueue_tx/outbox_enqueue_txstamp resolution usesstamps_with_overrideover the plain (300/3/5/none) and outbox (60/5/5) derived sets exactly as ADR-020 §3 pins; the stamp sets are named functions (one owner), resolution happens after entry-point validation and before any effect lands.
Summary
Filled on completion, 2026-10-10.
Landed the mem engine's seam: the complete Store trait surface wired
over the guarded state core — notify/listen (wake substrate bridge
with the pinned recv forms and close-at-engine-drop), stream/queue/
outbox/try_lock (validated constructors → boxed core-owned handle
traits carrying the mem mechanisms' full surfaces: ADR-010 queue state
machine with claims/reclaim/dead-letter/sweep and the equal-jitter
curve, ADR-015 log+offsets+trim+subscribe, ADR-008 §7 TTL registry,
ADR-014 outbox helper with run_once), schedule/unschedule/
run_schedules (ADR-009 collapse: @every-only third-owner parser, upsert
table, the 64-cap bounded catch-up + grid-aligned skip-forward tick
under the schedules→queues guard, leadership through the reserved
__alkstore_scheduler lock with renew-before-tick and the clean-stop/
LeadershipLost returns) — plus the load-bearing tx overlay: begin_tx
allocates the handle-private overlay; all eleven *_tx methods stage
with entry-point validation, Codec-typed encoding, one clock read,
ADR-020 §3 stamp resolution (plain 300/3/5/none, outbox 60/5/5, the
per-job max_attempts override); tx reads merge overlay-over-committed
(read-your-own-writes, queue-scoped + dead-visible get_job_tx,
monotone saves, offset-ASC reads, extent guards); commit is the one
guarded merge (queues → streams) followed by the wake-at-commit fire
(staged notifies + per-event stream wakes — nothing delivers pre-merge,
one wake per staged effect); drop (explicit or through an unwinding
panic) discards the overlay — no-ghosts by construction. Engine-side
arithmetic (resolution.rs) is mem's third-owner copy
(ADR-012 §2): stamp sets, delay-wins-over-run_at, relative-
expires, the reserved-prefix outbox derivation, stamps_with_override,
and the equal-jitter curve (std-only RandomState hash jitter, no
rand, pinned range).
resolution.rs — mem's third-owner arithmetic — landed with
the tx stamping and is what the mechanism tasks should consume for the
cross-engine equivalence rows (suite-pinned).
89 unit tests on alkstore-mem (17 foundations + 72 new): the overlay
discipline (staged-notify silence pre-commit → one wake post-commit,
stamps + commit-atomicity shapes, drop/rollback/panic-discard
no-ghosts across job/event/notify/offset, tx reads merged,
concurrent-tx merge order, with_tx Ok/Err/panic postures), numeric
domains (claim/read extents clamp, duration guards on lock paths,
negative-horizon trim, the @every grammar matrix, entry validation
across every surface), and the mechanism smoke pins (lifecycle,
ordering, validity predicate + late-heartbeat refusal, reclaim-eats-
attempt, dead-letter strings, sweep both-states + retention, stream
FIFO + trim + subscriber replay/wake + close arms, lock
contention/expiry, tick/catch-up/skip-forward, leadership contention +
clean stop, outbox ack/retry curves). Verified: workspace cargo test
green (core 25, suite-harness 3, mem 89, SQLite 124 lib + 191
suite, pg 124 lib + 25 suite), cargo clippy --all-targets -- -D warnings clean, cargo fmt --check clean, and
cargo check --target wasm32-unknown-unknown -p alkstore-mem clean —
the wave-6 wasm gate stays green.