hermes_state: opt-in in-place self-heal for a lost WAL generation (fixes #109687) - #109766
Open
Sahilvishnaliya wants to merge 1 commit into
Open
Sahilvishnaliya wants to merge 1 commit into
Sahilvishnaliya wants to merge 1 commit into
Conversation
…usResearch#109687) A short-lived reader's clean close unlinks the live gateway writer's -wal/-shm sidecars on Linux; the writer keeps serving while committing into the unlinked inode, and the shipped fail-closed halt is sticky for the life of the process. The capture machinery (NousResearch#105670) already preserves the retired frames; this adds the recovery the issue asks for: under database.wal_self_heal (default OFF, fail-closed posture unchanged), a halted writer captures the retired generation, drops its orphaned descriptors with the close-time checkpoint disabled, reopens through refuse_deleted_wal_generation exactly like a fresh process, and adopts the current generation — writes resume without operator intervention. Read-pool connections minted before the heal are evicted at checkout so no orphaned fd serves reads. Developed on Windows: cross-platform logic tests cover the gating, sticky-halt fallback, guard refusal, full adopt-and-resume flow and pool eviction; the real unlink/adopt end-to-end test is Linux-only (skipif), same gate as the existing deleted-WAL guard tests.
Closed
3 tasks
5 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #109687
The problem
A short-lived reader (one
sqlite3.connect()+ close, e.g.hermes sessions stats) unlinks the live gateway writer'sstate.db-wal/-shmon Linux. The gateway keeps serving while committing into the unlinked inode — session writes are silently discarded — and the shipped deleted-WAL guard then fails every other process closed. The halt is sticky for the life of the writer process: the only remediation is an operator killing all holders.What this adds
The issue's second explicit ask — "a writer that does lose its generation should be able to recover without operator intervention" — as an opt-in self-heal,
database.wal_self_heal: true(default OFF; the fail-closed posture is unchanged for unflagged installs):capture_retired_wal_generationmachinery (WAL generation split-brain: DeletedWalGenerationError guarded writes, but the graceful-shutdown checkpoint then corrupted state.db (field report) #105670) — nothing that was committed to the orphaned WAL is lost; the artifact stays for inspection.SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE); if the checkpoint cannot be suppressed (Python < 3.12), the heal fails closed and the shipped pin path stays.refuse_deleted_wal_generationexactly like a fresh process — if any other process still holds an orphaned sidecar, the guard refuses and the canonicalDeletedWalGenerationErrorstands. The heal never mints a second WAL while the box is not clear.Read-pool hygiene: pooled read connections minted before the heal hold fds into the orphaned generation; an epoch allowlist set at heal time evicts them at checkout (
_fresh_read_conns), so no stale descriptor ever serves reads afterwards.Design notes / honesty
state.dbWAL generation on Linux (reproduces on 2026-09-12main, after the #102589 fix) — gateway keeps serving while silently dropping session writes #109687 explicitly did not pin a line-level producer in Hermes, and several prior fixes (cron/lifecycle_guard raw-opens state.db when a command mentions its path, dropping the gateway's POSIX locks -> WAL split-brain #102589, state.db WAL split-brain inside a SINGLE process: apply_wal_with_fallback runs the set-pragma when the journal-mode probe fails (unlinks sibling connections -wal/-shm) #104596) chased specific producers while the bare 3-linesqlite3.connect()repro still orphans the WAL. This PR makes the victim recoverable instead of racing the next producer.skipif(not linux)— same gate as the existing guard tests intest_deleted_wal_generation_guard.py(23 of those skip on this host too). The Linux end-to-end path itself is not executed locally and needs CI's Linux runners to validate.tests/hermes_stateon this Windows host are identical on pristine main (inode-replacement/quarantine semantics that need Linux); the guard/capture suites pass; ruff clean; zero newtydiagnostics (diffed against base — only line-number shifts of the 9 pre-existing ones).Tests
tests/hermes_state/test_wal_generation_self_heal.py— 7 tests:test_flag_defaults_offtest_heal_off_keeps_fail_closed_halttest_heal_attempted_then_sticky_when_it_cannot_healtest_heal_refuses_when_guard_detects_foreign_holderstest_heal_adopts_current_generation_and_resumestest_pool_evicts_pre_heal_read_connstest_heal_recovers_writer_without_operator