SQLite engine: notify/listen — auto-commit notify, watcher-fanout WakeReceiver bridge (task sqlite-engine-notify-listen)

This commit is contained in:
glm-5.3-flash committed 2026-10-08 12:12:34 +00:00
1 parent c38033db1a
commit 8efe0c2e78
7 files changed
+795 -24

No files matched your search

+83 -10
View File
@@ -1,7 +1,7 @@
---
id: sqlite-engine-notify-listen
name: SQLite engine — notify/listen (watcher fanout → `WakeReceiver` bridge)
status: pending
status: completed
depends_on: [sqlite-engine-seam-tx]
scope: narrow
risk: medium
@@ -43,19 +43,19 @@ should pin wake-arrives and close-on-death, not wake-exclusivity.
## Acceptance Criteria
- [ ] `notify` delivers commit-atomic auto-commit notifies; payload
- [x] `notify` delivers commit-atomic auto-commit notifies; payload
crosses the notify table (no size limit — never
`PayloadTooLarge`)
- [ ] `listen` returns a working `WakeReceiver`: wakes arrive after
- [x] `listen` returns a working `WakeReceiver`: wakes arrive after
commits by other connections; `Wake { channel }` carries the
channel name only
- [ ] Watcher death closes the receiver (`recv() -> None`, terminal)
- [x] Watcher death closes the receiver (`recv() -> None`, terminal)
— tested via the substrate's death-signal path
- [ ] `try_recv`/`recv_timeout` arms match the pinned contract shapes
- [ ] Receiver drop unsubscribes (no leaked subscriptions —
- [x] `try_recv`/`recv_timeout` arms match the pinned contract shapes
- [x] Receiver drop unsubscribes (no leaked subscriptions —
`subscriber_count` returns to baseline)
- [ ] Channel validation on both entry points
- [ ] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt clean
- [x] Channel validation on both entry points
- [x] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt clean
## References
@@ -67,8 +67,81 @@ should pin wake-arrives and close-on-death, not wake-exclusivity.
## Notes
> To be filled by implementation agent
- **Bridge architecture**: the substrate's sync feed (1-slot
`sync_channel` — the coalescing point) → one dedicated
`std::thread`-via-`spawn_blocking` per subscription looping
`recv()` and `blocking_send`ing `Wake { channel }` into a tokio
mpsc channel (capacity 16 — smoothing only; the sync feed stays the
coalescer) that `SqliteWakeReceiver` owns. The bridge thread exits
on either side's disconnect: subscriber feed closed (watcher death,
`WatcherDeathGuard`) or consumer-side `WakeReceiver` dropped.
- **Unsubscribe eagerly on drop, not lazily**: the substrate prunes
disconnected subscribers lazily (at the next fanout), which could
strand the bridge thread if a consumer drops the receiver on an
idle db (no future fanout ever comes → the sender never sees the
disconnect). `SqliteWakeReceiver`'s `Drop` calls
`watcher.unsubscribe(id)` — immediate teardown, tested via
`subscriber_count` returning to baseline. The substrate doc on
`subscribe` ("callers MUST unsubscribe") is honored at the bridge
layer.
- **`closed` flag on the store**: `SqliteStore` gained an
`Arc<AtomicBool> closed` set by `close()`; `listen` checks it and
fails closed with `Database` (matching `begin_tx`'s writer-slot
shape). Needed because `SharedUpdateWatcher::subscribe` on a closed
watcher would still succeed mechanically (it re-inserts into the
senders map) while never receiving — a silent-dead subscription.
`notify` on a closed store fails closed through
`Writer::acquire() -> None` (no new machinery).
- **`with_writer` seam helper (new, `seam.rs`)**: the short-lived
writer-slot lease for auto-commit ops — acquire, op in
`spawn_blocking`, release; the slot is free the instant the op
completes (no lease held across consumer `await` points — that is
the long-tx posture, not this path). Reusable by later auto-commit
mechanism tasks (locks/streams/queues constructors' read paths can
route through it or the reader pool as their tasks decide).
- **Death-signal test driver**: the death path is pinned via store
`close()` (the ordinary death — the death-guard clears every
subscriber feed). The file-replacement death (the fork's
dead-man's-switch) is covered at the substrate layer already
(`shared_update_watcher_signals_subscribers_on_watcher_death`);
the bridge only sees "feed closed" either way.
- **Rollback-wakes-nothing**: pinned indirectly — a rollback does not
bump `data_version` (substrate-pinned), so no wake follows the
`notify_tx`-then-drop path; the test drains first (coalescing makes
an exact pre-drain assertion racy, per the wake-hint posture) and
requires quiet-after-settle instead.
- **`tokio` `time` feature added** to the engine crate's main deps
(`recv_timeout`'s `tokio::time::timeout`) — previously dev-only.
- **Store validation ordering**: `listen` validates the channel before
the closed check (validation errors are the contract-shaped answer
even on a closed store — the rejects never touch the engine);
`notify` validates before encoding the payload.
## Summary
> To be filled on completion
Implemented the wake half of the notify mechanism on the SQLite
engine: `Store::notify` (auto-commit — writer-slot acquire, the
substrate's `notify()` SQL function in one auto-commit transaction,
release; payload crosses `__alkstore_notifications` as the serde_json
serialization's UTF-8 text; no size limit — ADR-016 §5's
never-`PayloadTooLarge` pinned by a 2 MB payload test) and
`Store::listen` (the watcher-fanout bridge: substrate
`SharedUpdateWatcher::subscribe` per subscription, one
`spawn_blocking` thread doing `blocking_send` of
`Wake { channel: … }` into a tokio channel owned by the boxed
`SqliteWakeReceiver`; eager unsubscribe on drop). Close semantics
pinned: watcher death / store close ⇒ `recv() -> None` terminal
(never reopens); `try_recv` distinguishes `Err(Closed)` from idle
`Ok(None)`; `recv_timeout` expiry without a wake is the documented
`Ok(None)` arm, death is `Err(Closed)`. Entry-point validation
(`InvalidName`/`ReservedName`) on both paths, before any round trip
or payload encoding. New seam helper `with_writer` (short-lived
writer-slot lease for auto-commit ops). 13 new tests (auto-commit row
crossing, no-limit, wake-arrives cross-connection, channel-only
content, death-close terminal + never-reopens, try_recv arms,
recv_timeout arms, unsubscribe no-leak, drop-closes, validation
×4 shapes both entry points ×no side effects, multi-listener fanout,
closed-store fail-closed, tx-notify wakes-at-commit). Verified:
`cargo test --workspace` (25 + 3 + 146 sqlite, all green), suite run
3× green, `cargo clippy --all-targets -- -D warnings` clean,
`cargo fmt --check` clean.