Sweep of docs/reviews/ and tasks/ per the plan's §Wave 8 historical note: 'wave 7'-means-release-pass references annotated or reworded per the review-wave-5.md renumber-annotation precedent; genuine fuzzing-wave/review-7 references untouched. Out-of-scope architecture-doc trail records documented in the task's Notes. 12 annotated, 1 reworded (live pointer); taskgraph validate green.
11 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pg-fix-scheduler-fire-wake | pg scheduler — fire wakes: the tick's in-tx enqueue fires issue their pg_notify wake (the recorded latency-parity gap, closed) | completed | narrow | low | component | implementation |
|
Description
Close the recorded cross-engine scheduler fire-wake latency-parity gap —
the one item the wave-5 review gate left standing "for a later task"
(tasks/review-wave-5.md §Notes 4, audit verdict: "the recorded gap
stands for a later task; accepted as-is"). Provenance chain:
tasks/review-wave-4-fixes.md §"Recorded for later" (the original note:
"Scheduler fire wakes (the tick's in-tx enqueue) issue no pg_notify
— SQLite's watcher wakes on those commits… if wave 5's cross-engine
equivalence rows demand the parity, it is a one-call wake_tx inside
the tick's tx"), dispositioned not-demanded by
tasks/suite-scheduler-rows.md §Notes (the suite rows observe fires via
claim polling; wake latency out of scope), and audited as-is by the
wave-5 gate. Correctness was never at stake — the queues row's pinned
re-poll safety net covers it on both engines — but the asymmetry stands:
a schedule fire on pg is invisible to a registered listener until the
listener's re-poll, while on SQLite the watcher wakes that queue's
listeners at the tick's commit.
Site: alkstore-postgres/src/scheduler.rs:518-548 — the tick's
one-firer-per-boundary loop calls enqueue_row(client, …) inside the
[crate::queue::in_tx] frame (ADR-009 §4's layer (b)) and issues no
wake. The fix is the recorded one-call shape, riding the
pg-fix-tx-wake pattern of record (tasks/pg-fix-tx-wake.md):
tx::wake_tx (currently private at tx.rs:287 — widen to
pub(crate)) issued inside the tick's in_tx frame, so NOTIFY's
native transactional delivery makes the wake commit-atomic — a rolled-
back tick (crash mid-tick ⇒ the boundary refires) discards the wake
with its fire rows, exactly the tx/no-ghosts discipline the tx producer
paths already pin.
Fix constraints (pinned):
- Channel = the fired queue's name (the mechanism-name-is-the-channel
realization;
schedule()rejects reserved names per ADR-021 §3, so a fire's target can never be a derived outbox channel — the plain queue name is always the right channel). - Empty payload —
Wake { channel }only (ADR-008 §3); best-effort log-and-swallow posture (wake_tx's existing stance; a wake must never fail a tick — ADR-006). - Coalescing default: one wake per tick per schedule that fired ≥ 1
(after the fire loop, still inside the
in_txbody), not per-fire — wakes are hint-only (ADR-006; no per-wake semantics exist to preserve), and this matches the SQLite twin's shape (the substrate watcher wakes per committed tick transaction, not per enqueued row). Deviate to per-fire only with the reason recorded in Notes. - No double-wake risk: the wake is applied at the tick call site, NOT
inside the shared
enqueue_row(thepg-fix-tx-wakeone-owner posture —enqueue_rowstays wake-free; the auto-commitQueue::enqueueand the tx paths keep their own single wakes, untouched).
Test shape (engine-side, pg): a harness test in the scheduler tests
— a schedule registered against a queue, a listener registered on that
queue's channel (forwarder().register; listener_hears), the runner
ticks with the schedule due: the fire wake arrives at/after the tick's
commit (pre-commit silence window to pin commit-atomicity, the
tx_producer_wakes_are_commit_atomic_and_rollback_discards_them
pattern); a tick with nothing due issues no wake. The rollback arm is
native (NOTIFY's transactional delivery — already proven by notify_tx
and the tx-wake test); do not build a tick-fault seam for it here
(pg-fix-scheduler-tick-seam's seam, if landed first, is the retry
loop's, not this site's — same file, disjoint site, either landing
order is fine).
Record updates (the gap is recorded in three places — retire all
three): tasks/review-wave-4-fixes.md's "Recorded for later" bullet
(marked retired by this fix), tasks/suite-scheduler-rows.md §Notes'
fire-wake-parity disposition (gap closed, pointer), and
tasks/review-wave-5.md §Notes 4's audit-table row (pointer to the
closing task). Add the review-rounds line to
docs/plans/implementation.md.
Ordering note: no hard dependency on wave 6, but landing before
mem-engine-scheduler-outbox keeps the mem engine's fire posture
written against a closed gap (mem's in-memory wake routing is trivially
immediate; the suite's three-way story then starts symmetric).
Acceptance Criteria
- The tick's fire path issues its wake commit-atomically inside the
in_txframe (channel = the fired queue's name, empty payload, best-effortwake_txposture); the coalescing decision recorded in Notes (default: one wake per firing schedule per tick) - Pinned by test: a listener on the target queue's channel receives the wake at/after the tick's commit (pre-commit silence window); a tick with nothing due issues no wake
- No wake regression: auto-commit
Queue::enqueue, the tx producer paths, andenqueue_rowitself unchanged (no double-wake) - The three gap records retired with pointers
(
review-wave-4-fixes,suite-scheduler-rowsNotes,review-wave-5§Notes 4) + theimplementation.mdreview-rounds line cargo test -p alkstore-postgresgreen against the harness server; skips clean server-less; clippy-D warnings; fmt clean; workspace gates green
References
- tasks/review-wave-4-fixes.md §"Recorded for later" (the original recorded note, incl. the one-call shape)
- tasks/suite-scheduler-rows.md §Notes (the not-demanded disposition)
- tasks/review-wave-5.md §Notes 4 (the gate's audit row)
- tasks/pg-fix-tx-wake.md (the wake_tx pattern + commit-atomicity
precedent;
tx.rs:287) - docs/architecture/decisions/006-wake-and-delivery-contract.md (wakes best-effort, hint-only)
- docs/architecture/decisions/009-scheduler-collapse.md §4 (the tick's tx frame)
- docs/architecture/engine-postgres.md (pg_notify as the wake trigger)
Notes
Fix-shape decisions of record (what the description left open):
- Coalescing: the default — one wake per tick per schedule that
fired ≥ 1, issued after the fire loop (before the boundary-advance
UPDATE), inside the caller's
in_txframe. Not per-fire: wakes are hint-only (ADR-006, no per-wake semantics exist to preserve), and the SQLite substrate's watcher wakes per committed tick transaction, not per enqueued row — the parity shape is the committed tick, not the individual fire. Thefor row in &dueloop naturally yields one wake per firing schedule (multi-schedule ticks wake each schedule's own channel once). A quarantined row (continues before the fire loop) fired nothing and wakes nothing. No per-fire deviation. - Wake placement & ownership: the wake is issued at the tick call
site in
scheduler.rs::tickvia the sharedtx::wake_tx(widened private →pub(crate)exactly as the task pinned);enqueue_rowstays wake-free (no double-wake), the auto-commitQueue::enqueueand the tx producer paths untouched. Channel = the fired queue's name; empty payload (Wake { channel }, ADR-008 §3); best-effort — a swallowed wake failure aborts the tx server-side so the truth surfaces typed at COMMIT (and the tick rolls back with its fires). - No wake issued for a due row whose fire count is zero-guarded —
no guard added: a non-quarantined due row fires ≥ 1 by construction
(the
while next_fire_at <= nowloop runs at least once), so the unconditional post-loop wake is exactly "per schedule that fired ≥ 1". - Test shape: one harness test in
scheduler_tests.rs(scheduler_fire_wakes_are_commit_atomic_coalesced_and_nothing_due_is_silent), four arms: (a) pre-commit silence — the tick driven directly through the runner's ownin_txframe (therogue_concurrent_ticksshape; the silence window needs the tick body observable before its COMMIT, which the runner's loop holds internally), listener silent statement-side, hears at/after commit; (b) coalescing — backdated one interval ⇒ ≥ 2 boundaries fire in the one tick, exactly one wake (not per fire); (c) nothing-due silence — a same-tick not-due schedule and a later all-not-due tick wake nobody (in-frame and post-commit); (d) end-to-end runner leg — a liverun_schedulesleader's fire wake reaches a registered listener. The direct-tick drive is the runner's exact frame (tick_once=in_tx+tick); the runner loop itself is separately exercised by the existing fire tests and by arm (d). A locallistener_hearshelper copy rides the per-file helper convention (tx_tests.rskeeps its own). The rollback arm was not test-built here (native — NOTIFY's transactional delivery, proven bynotify_txand the tx-wake test; the task pinned no tick-fault seam for it). - Doc updates rode along:
tx.rs's "The tx wakes" module section andwake_tx's doc now name the scheduler fire path as a shared caller;scheduler.rs's module docs andtick's doc pin the coalesced commit-atomic fire-wake semantics (the contract-relevant text). - Flake-ledger occurrence (unrelated to this task's mechanism):
during the first full pg suite run this session,
row_lock_ttl_expiry_and_reacquisition(contract suite) failed once — the wave-5 ledger's second recorded occurrence. Green on the next two full runs plus six focused runs. Recorded intasks/review-wave-5.md§6 with the ledger's own conclusion: the "fails again" trigger is met, a dedicated investigative session is owed by the release-readiness watch carriage (wave 7 at writing — wave 8 since the fuzzing wave's renumber) before any postulate-and-fix. Not load-bearing here (locks, not scheduler/wakes; scheduler code untouched by that row).
Summary
Landed: the scheduler tick's commit-atomic fire wake —
scheduler.rs::tick now issues one coalesced pg_notify per firing
schedule per tick (channel = the fired queue's name, empty payload)
inside the tick's in_tx frame via the shared tx::wake_tx (widened
to pub(crate)); the new four-arm pinning test; the driver docs
(tx.rs wake section, scheduler.rs module + tick docs); and the
three gap records retired with pointers (review-wave-4-fixes's
recorded-for-later note, suite-scheduler-rows's gate disposition
note + Notes bullet, review-wave-5 §Notes 4's audit row) plus the
implementation.md review-rounds line — the three-way (SQLite / pg /
mem-to-be) fire-wake story is symmetric ahead of wave 6.
Verified: full pg lib suite 122/122 green against the harness
server (the new test deterministically green across three solo runs);
pg contract suite 25/25 and schema 9/9 green (one unrelated single-run
flake noted above, ledger-carried); workspace cargo test green
server-less (pg skips clean, incl. the new test); cargo clippy --all-targets -- -D warnings clean; cargo fmt --check clean.