sqlite: dead-watcher subscribe fails closed + initial-stat identity-unknown baseline (review 004 items 2+3) — the poll loop sets a dead sign before the watcher-fatal exit and subscribe() fails closed on it (and the taken watcher slot) atomically with enrollment; listen/stream subscribe map it to Error::Database, never park on a dead feed; the identity baseline is Option-tracked so a failed startup stat no longer seeds the (0,0) false identity that a transient recovery would trip into death; engine-sqlite.md posture covers both arms; PROVENANCE D-39; task -> completed

This commit is contained in:
glm-5.3-flash committed 2026-10-11 10:38:10 +00:00
1 parent 22f25ffafd
commit 0fadf0f224
10 files changed
+475 -57

No files matched your search

+80 -7
View File
@@ -1,7 +1,7 @@
---
id: sqlite-fix-watcher-death-subscribe
name: Dead-watcher subscribe hang + initial-stat self-kill (review 004 items 2+3)
status: pending
status: completed
depends_on: []
scope: moderate
risk: medium
@@ -46,17 +46,17 @@ subscribers stay open.
## Acceptance Criteria
- [ ] Subscribing (listen or stream) after watcher death fails closed
- [x] Subscribing (listen or stream) after watcher death fails closed
with a typed error instead of parking — pinned by test
- [ ] Pre-death subscribers still close at watcher death — the
- [x] Pre-death subscribers still close at watcher death — the
existing pins green unchanged
- [ ] An initial identity-stat failure no longer seeds a false
- [x] An initial identity-stat failure no longer seeds a false
identity: the first successful stat does not trigger the death
path and subscribers stay open — pinned by injected-failure test
- [ ] engine-sqlite.md's watcher-death posture text covers the
- [x] engine-sqlite.md's watcher-death posture text covers the
new-subscriber arm (and the identity-unknown state if it changes
observable behavior)
- [ ] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt
- [x] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt
clean; workspace gates green server-less
## References
@@ -71,6 +71,79 @@ subscribers stay open.
> To be filled by implementation agent
Decisions of record the description didn't pin:
- The dead sign lives on `SharedUpdateWatcher` as an
`Arc<AtomicBool>`, set by `run_poll_loop` itself just before the
watcher-fatal return — it is set *before* the death-guard's senders
clear (which happens after the loop returns), so the guard-side
ordering is fixed by program order.
- `SharedUpdateWatcher::subscribe` became fallible
(`Result<_, String>`) and checks **two** closed arms atomically with
the enrollment under the senders lock: the dead sign *and* the
taken watcher slot (the post-`close` enroll-onto-empty-watcher race
of the same shape). Failing closed on the taken slot is new
belt-and-braces beyond the finding's shape; the store's `closed`
flag already guarded the normal path. Rationale for checking under
the lock: a check-then-enroll done outside the lock could interleave
between the death and the guard's clear and enroll a ghost.
- The substrate stays contract-blind: the failure is a `String`
naming the death, mapped into the taxonomy engine-side —
`Error::Database` (the opaque fallback with the source chain), the
same shape the engine's closed-store checks use. `Error::Closed`
was rejected: the contract docs scope that variant to *receiver*
closed arms (`try_recv`/`recv_timeout`), not entry points.
- The initial-identity fix is an in-place re-derivation of the same
code (`Option<(u64, u64)>` baseline vs. the `(0, 0)` seed); the
death-log message keeps its precise diagnostic (now printing the
captured baseline).
- The injected-failure test for (b) is the substrate-level drive
(`run_poll_loop` with the db file absent at start — the initial
stat genuinely fails; on non-unix `stat_identity` never fails, so
the scenario cannot arise and the test is unix-gated like the other
file-replacement drives); no `cfg(test)` injection seam was added to
`stat_identity` itself. Aliveness after the first successful stat is
proven by wake delivery relative to a pre-insert snapshot (failing
identity stats fire `on_change`, so absolute wake counts are not a
clean liveness signal).
- Also updated: engine-sqlite.md's watcher-failure posture bullet
(both arms), notify.rs/stream.rs/store.rs module docs, open_tests'
three direct `watcher.subscribe()` call sites (now `.unwrap()`d),
and one PROVENANCE.md row (D-39 — the register wants divergence to
the ported watcher recorded even though the task text didn't name it).
## Summary
> To be filled on completion
> To be filled on completion
Both defects landed with tests:
- (a) Post-watcher-death subscribe hang: the poll loop sets the dead
sign before exiting through the identity-replacement death path;
`SharedUpdateWatcher::subscribe` fails closed on it (and on a taken
watcher slot) atomically with enrollment under the senders lock;
`notify.rs::listen` and `stream.rs::spawn_bridge` map the failure to
`Error::Database` with the death named in the source chain. Pinned
by four tests: substrate-level
(`subscribe_fails_closed_after_watcher_death`, unix file-replacement
drive) and engine-level (`listen_after_watcher_death_fails_closed`,
`subscribe_after_watcher_death_fails_closed` — same drive through
the store surface, error-shape + no-enrollment assertions). The
pre-death subscriber close-at-death pins
(`watcher_death_closes_the_receiver_terminally`,
`watcher_death_closes_the_subscription_terminally`, and the
substrate death-signal tests) are green unchanged — the death-signal
substrate test also now pins the dead sign being set.
- (b) Initial-stat self-kill: the loop tracks the identity baseline as
`Option<(u64, u64)>`; a failed startup stat leaves it unknown, the
first successful stat captures it without driving death, and a later
genuine mismatch still kills. Pinned by
`initial_stat_failure_does_not_seed_a_false_identity`
(`run_poll_loop` drive: absent file → unknown baseline → file
created → no death + wake delivery lands → replacement → death
fires; unix-gated).
- Verified: `cargo build`, `cargo test` (workspace, all green),
`cargo test -p alkstore-sqlite` (201 tests), fuzz gate
(`cargo test --manifest-path fuzz/shared/Cargo.toml`), clippy
`-D warnings` clean, `cargo fmt --check` clean, rustdoc
`-D warnings` doc gate clean.