295edf2557
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>
41 lines
1.6 KiB
Python
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()
|