Files
hermes-agent/tests/hermes_state/test_wal_active_confirmed.py
T
teknium1 295edf2557 fix(state): gate the lock-free read pool on a confirmed WAL header, not the assumed mode
apply_wal_with_fallback() reports "wal" in two indeterminate cases -- the vulnerable-
SQLite gate (_apply_delete_for_wal_reset_bug) and the non-vulnerable probe-unknown path
(a7f2a593d1) -- meaning "touched nothing, the connection inherits the header's mode".
SessionDB turned that assumption into `_wal_active=True`, which enables the mode=ro read
pool that skips `self._lock`. On a file that is really in rollback-journal mode those
readers race the writer with a 5s busy timeout and no retry: random SQLITE_BUSY read
failures for the instance's lifetime (#86515).

Confirm the header on the freshly opened connection before enabling the pool. When the
probe is still blocked, reads queue on the writer connection under the lock -- slower,
never wrong. Every other apply_wal_with_fallback caller ignores the return value, so the
consumer is the right place to gate; changing the return contract to Optional across
15 call sites (#87044's shape) is not needed.

Live repro: DELETE-mode file, sibling holding BEGIN EXCLUSIVE during open ->
before: _wal_active=True and _checkout_read_conn() hands out a pooled mode=ro conn;
after: _wal_active=False, reads take the locked writer path.

Fixes #86515. Based on the analysis in #87044.
Co-authored-by: QDung210 <dqdung205@gmail.com>
2026-09-11 06:23:23 -07:00

41 lines
1.6 KiB
Python

"""#86515: the lock-free mode=ro read pool needs a CONFIRMED WAL header. When the on-disk probe at open
is blocked by a concurrent opener, apply_wal_with_fallback reports "wal" as the assumed mode with nothing
touched; SessionDB must not turn that assumption into pooled readers on a file that is really DELETE."""
import sqlite3
from hermes_state import SessionDB
def test_blocked_probe_on_delete_file_does_not_enable_read_pool(tmp_path):
path = tmp_path / "state.db"
db = SessionDB(db_path=path)
db.create_session("s", "cli")
db.close()
offline = sqlite3.connect(str(path)) # sole opener: a DELETE switch here is safe
assert offline.execute("PRAGMA journal_mode=DELETE").fetchone()[0].lower() == "delete"
offline.close()
sibling = sqlite3.connect(str(path), isolation_level=None)
sibling.execute("BEGIN EXCLUSIVE") # blocks the header read: the probe comes back None
try:
conn = db._open_writer_conn()
finally:
sibling.execute("ROLLBACK")
sibling.close()
try:
assert db._wal_active is False
assert db._checkout_read_conn() is None # reads queue on the writer lock, never SQLITE_BUSY
finally:
conn.close()
def test_confirmed_wal_file_keeps_read_pool(tmp_path):
path = tmp_path / "state.db"
db = SessionDB(db_path=path)
try:
if sqlite3.connect(str(path)).execute("PRAGMA journal_mode").fetchone()[0].lower() != "wal":
return # vulnerable-SQLite or WAL-refusing filesystem: nothing to confirm
assert db._wal_active is True
finally:
db.close()