fix: prevent state.db B-tree corruption from WAL TRUNCATE checkpoint#45546
Conversation
state.db suffered recurring B-tree corruption on large FTS5 databases (255 MB, ~65K pages, 347 sessions). Root cause: _try_wal_checkpoint() used PRAGMA wal_checkpoint(TRUNCATE) every 50 writes, which acquires an exclusive lock, transfers all WAL frames, syncs the DB, and calls ftruncate() to shrink the WAL to zero bytes. On large FTS5 shadow tables, the ftruncate() can race with pending page writes, leaving B-tree pages with invalid references (btreeInitPage error 11, invalid page numbers). Three changes: 1. Switch TRUNCATE to RESTART — same frame transfer but without ftruncate(), eliminating the race while still preventing unbounded WAL growth. TRUNCATE retained only in close() and vacuum() where the exclusive lock makes it safe. 2. Disable SQLite auto-checkpoint (PRAGMA wal_autocheckpoint=0) so the internal PASSIVE checkpoints don't compete with our explicit RESTART strategy on large databases. 3. Update stale comments to reflect the new checkpoint strategy. Closes NousResearch#45383
|
The corruption diagnosis is solid — My concern is the fix's |
Verification: Clean fix — RESTART checkpoint avoids FTS5 B-tree corruptionReviewed the checkpoint mode change (
Well-structured, minimal-risk change with good documentation. |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Important fix for state.db WAL checkpoint corruption (Issue #45383). The periodic TRUNCATE WAL checkpoint could cause B-tree corruption on large FTS5 databases (65K+ pages) due to the ftruncate race. This fix:
- Changes periodic checkpoints from TRUNCATE to RESTART mode (same frame transfer and header reset, no ftruncate)
- Disables SQLite's built-in PASSIVE auto-checkpoint to avoid redundant I/O
- Keeps TRUNCATE only in close() and vacuum() where exclusive access is guaranteed
- Adds detailed comments explaining the root cause and the RESTART vs TRUNCATE tradeoffs
Well-researched fix. No concerns.
Reviewed by Hermes Agent
Summary
state.dbsuffered recurring B-tree corruption on large FTS5 databases (255 MB, ~65K pages, 347 sessions, ~25K messages). Observed twice in 24 hours with identical corruption patterns.Root Cause
_try_wal_checkpoint()inhermes_state.pyusedPRAGMA wal_checkpoint(TRUNCATE)every 50 writes. TRUNCATE mode:ftruncate()to shrink the WAL file to zero bytesOn large FTS5 shadow tables, the
ftruncate()can race with pending page writes, leaving B-tree pages with invalid references — manifesting asbtreeInitPage() returns error code 11andinvalid page number 218103808.Additionally, SQLite's built-in auto-checkpoint (default: PASSIVE every 1000 pages) was running concurrently, creating redundant I/O and lock contention.
Fix
PRAGMA wal_autocheckpoint=0— stops SQLite's internal PASSIVE checkpoints from competing with explicit RESTART.close()(connection teardown under lock) andvacuum()(full DB rewrite).Changes
hermes_state.pyVerification
test_hermes_state.py+test_hermes_state_wal_fallback.py: ✅test_hermes_state_compression_locks.py+test_state_db_malformed_repair.py: ✅Closes #45383