SQLite engine: notify/listen — auto-commit notify, watcher-fanout WakeReceiver bridge (task sqlite-engine-notify-listen)
This commit is contained in:
1 parent
c38033db1a
commit
8efe0c2e78
7 files changed
+795
-24
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user