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:
1 parent
22f25ffafd
commit
0fadf0f224
10 files changed
+475
-57
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user