fix(state): optional self-skip in deleted-WAL holder scan breaks post-halt self-deadlock - #109997
boomzikazita wants to merge 1 commit into
Conversation
…-halt self-deadlock
Related: #109966 / #109687 (deleted-WAL generation refusal cluster). Complementary open fixes touching the same guard: #109766 (opt-in in-place self-heal for a lost generation) and #109758 (macOS holder enumeration via libproc). Different mechanisms, so not marked duplicate; flagging so reviewers see the whole set. |
|
Production follow-up (same day): this patch fixes the self-refusal deadlock as designed, but soak testing exposed its boundary — it does not address inter-process WAL generation turnover. What we observed after deploying it: with the gateway in WAL, the dashboard process ( Final production resolution: This PR's updated positioning: insurance for deployments that stay on WAL (prevents the post-halt self-deadlock); operators on multi-process layouts should prefer |
|
Thanks @boomzikazita for the production report — closing this PR.
|
Problem
On Python < 3.12, a connection whose WAL generation was lost is retired unclosed by design (
_prepare_connection_retirementhas nosetconfig— closing would checkpoint retired frames onto the newer generation). The orphaned-wal/-shmdescriptors therefore linger in/proc/self/fdfor the lifetime of the process.iter_deleted_sqlite_sidecar_holders()counts this PID as a holder. After the process's first halt,refuse_deleted_wal_generation()— called before every freshsqlite3.connect— scans/proc, finds those dead retired descriptors, and refuses the process's own new connections. In production this loops every 5–10 minutes; restarting only resets the cycle (the next halt leaks the next generation).Repro layout: one
state.dbopened concurrently by gateway + dashboard + CLI sessions, all in WAL mode. Any process whose connections all close briefly unlinks the sidecar; every other process's still-open connections become orphaned-generation holders on their next write → halt → retire-unclosed leak → self-refusal.Fix
HERMES_WAL_GUARD_SKIP_SELF=1makes the holder scan skip this PID.Why skipping self is safe here
_wal_generation_was_lost()(capture + raise).Alternative considered
Tracking retired fd numbers in SessionDB and excluding exactly those in the scan (instead of the whole PID) — finer-grained, but requires threading retire state into the scan; the env-gated whole-PID skip is the minimal change and matches the existing fail-closed default (unset env = current behavior).
Test plan
tests/hermes_state/test_deleted_wal_generation_guard.py— 15 passed, 3 skippedtests/hermes_state/test_retired_wal_generation_capture.py— included above