Files
alkstore/tasks/mem-engine-seam-tx.md
T
glm-5.3-flash 1c3e96802f mem-engine-seam-tx — the wave's load-bearing seam lands: the complete Store/TxHandle trait surfaces over the guarded state core, with the tx overlay as the discipline centerpiece. Wider than the sqlite/pg seam twins by design (those rode pre-ported substrate ops and left mechanism methods stubbed; mem has no substrate, and the task's acceptance pins the full trait surfaces implemented over guarded state — a Rust trait impl is all-or-nothing per method): the seam task therefore realizes the mechanisms' committed-state effect layer and every mechanism handle itself, and the wave-6 mechanism tasks now own their acceptance-criteria unit pins against this foundation. Store surface: notify (codec-typed encode as the entry check only — the wake delivers the channel alone; PayloadTooLarge never produced, the ADR-016 §5 non-occurrence arm pinned at 2 MiB), listen (WakeReceiver bridge with the pinned recv forms; attach starts from now, one close path at engine drop), stream/queue/outbox/try_lock (validated constructors → boxed core-owned handle traits: the ADR-010 queue state machine with claims/reclaim-eats-attempt/uniform validity predicate/dead-letter/sweep + mem's third-owner equal-jitter curve — std-only RandomState hash jitter, no rand; ADR-015 log+offsets+trim+wake-driven subscribe; ADR-008 §7 TTL registry with renew-past-expiry refused; ADR-014 outbox helper with run_once ack/retry), schedule/unschedule/run_schedules (ADR-009 collapse: @every-only third-owner parser + register-table with pre-parsed interval and resolved stamps; the 64-cap bounded catch-up + grid-aligned skip-forward tick fired-and-advanced under one guarded op (schedules→queues lock order); leadership through the reserved __alkstore_scheduler lock with runner-unique owner, renew-before-tick, keep-awake sleep renewing on cadence, LeadershipLost / clean-stop-Ok returns). Tx overlay: begin_tx allocates the handle-private overlay; all eleven *_tx stage with entry-point validation before any effect, Codec-typed encode, one clock read, ADR-020 §3 stamp resolution (plain 300/3/5/none, outbox 60/5/5, the per-job max_attempts override via resolution.rs's stamps_with_override); tx reads merge overlay over committed (read-your-own-writes: queue-scoped + dead-visible get_job_tx, monotone staged saves, offset-ASC merged reads, extent guards clamping empty); commit consumes the handle, merges under one guarded op (queues→streams, the canonical lock order documented in state.rs), and fires the overlay's pending wakes AFTER the merge — wake-at-commit structural from birth (the pg-fix-tx-wake failure shape cannot exist here); drop (explicit, or through an unwinding panic — the overlay is plain local data, nothing runs on discard) = rollback with no ghosts, trivially. Stage-time id/offset allocation (the pg-shaped story — rolled-back allocations gap out; claims order id ASC, offsets immutable/never renumbered). Stream subscribers ride a reserved-prefix engine-internal wake channel (__alkstore_stream_wake:{stream}) unreachable by consumer entry points, attached before the stored-offset read; notify's transport stores nothing. 89 mem unit tests: the overlay discipline rows the task pins, the numeric-domain rows (extents/durations/boundaries/grammar/validation), and the mechanism smoke pins (the defect-class catch: ack/ack_batch originally deleted before checking the predicate — fixed; a refusal must leave the row exactly as it was). Manifest: serde_json = 1 joins (the trait signatures cross Value; both engine twins carry it; not a driver dep — wasm32 gate stays green). Task file: status completed, Notes (the whole-engine scope disposition + 11 decision-of-record entries incl. lock-renewal delta vs the sqlite substrate, ScheduleRow shape, runner lease mechanics, substrate predicate parity) + Summary filled. Verified: cargo build; workspace cargo test green (core 25, suite harness 3, mem 89, SQLite 191 lib + 25 suite, pg 124 lib + 25 suite + 9 schema); clippy --all-targets -D warnings clean; cargo fmt --check clean; cargo check --target wasm32-unknown-unknown -p alkstore-mem clean
2026-10-10 11:55:39 +00:00

255 lines
14 KiB
Markdown

---
id: mem-engine-seam-tx
name: Mem engine — tx overlay, `Store` surface, `TxHandle` (staged `*_tx`, commit-merge + wake-at-commit, drop = discard)
status: completed
depends_on: [mem-engine-foundations]
scope: moderate
risk: high
impact: component
level: implementation
tags: [wave-6, mem-engine, seam]
---
## 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.
- **`Store` surface**: 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 `*_tx` methods 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_tx` behaviors 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](../architecture/decisions/020-enqueue-opt-semantics-and-bridges.md)
§3: `enqueue_tx` stamps plain derived defaults 300/3/5/none;
`outbox_enqueue_tx` the 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_tx` composition: `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](../architecture/decisions/023-fourth-review-round.md)
§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
- [x] The full `Store`/`TxHandle` trait surfaces implemented over
guarded state; no engine types in consumer signatures
- [x] 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)
- [x] Drop-without-commit and explicit rollback leave zero residue;
`with_tx` panic rolls back through the unwind's drop
- [x] Tx reads see staged effects; other views see none pre-commit;
post-rollback reads' subjects gone (no-ghosts for reads)
- [x] Numeric-domain guards at entry (extents/durations/boundaries)
pinned per [ADR-023](../architecture/decisions/023-fourth-review-round.md) §2
- [x] `cargo check --target wasm32-unknown-unknown -p alkstore-mem`
clean; 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.
1. **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`/`TxHandle` trait 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.
2. **`serde_json = "1"` joined the manifest** (both engine twins carry
it): the trait signatures cross `serde_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.
3. **Stage-time id/offset allocation — the pg-shaped story**: `enqueue_tx`
/`publish_tx` return 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 yield
`offset 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).
4. **Canonical guard order `schedules → queues → streams`** (locks and
wakes depend on no other section; documented in `state.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).
5. **Merge mechanics**: `merge_and_wake` drains 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 consumer `listen`/`notify` can 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).
6. **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 the `Codec`-typed validation
(sqLite's transport row has no mem analog). `PayloadTooLarge` is
never produced at any size (the ADR-016 §5 non-occurrence arm,
pinned by a 2 MiB test).
7. **Lock semantics vs the SQLite substrate**: mem pins renew-past-
expiry *refused* (`false`) — the SQLite substrate's `lock_renew`
would 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_lock` grants without refreshing the deadline (the
`INSERT 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 (no `lock_tx` —
contract verbatim).
8. **`ScheduleRow` carries the pre-parsed interval + resolved stamps**
— not the raw spec/name pair. The name is the map key (upsert by
name) and `parse_every_interval` runs 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 the
`ScheduleOpts::{priority, expires}` riding themselves, resolved
against the fire instant (one clock read per tick).
9. **Runner lease**: owner is runner-unique (`scheduler-leader-{
instance}-{subsecond-nanos}` — an instance counter instead of
`process::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-internal
`run_tick(now)` with the frozen clock, deterministic without
realtime waits.
10. **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 `<=`, sweep `expires_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's
`DELETE … WHERE` checks per row); fixed, and the uniform-lapse
refusal tests now pin it.
11. **Sweep scoping note for later tasks**: `sweep_expired` counts both
the dead-letter moves *and* the retention deletions in its return
(substrate parity: `live_moved + retained`). A row whose
`expires_at` has already passed at enqueue is not claimable and
not pre-claim-dead-letterable — it sits for `sweep_expired`
(the pre-claim sweep only takes exhausted *claimable-shaped* rows).
12. **`enqueue_tx`/`outbox_enqueue_tx` stamp resolution** uses
`stamps_with_override` over 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.