5.0 KiB
querylog.db: wal_autocheckpoint = 8192
One constant. The v0.0.7 batching cut process writes from ~0.5 to 0.281 GiB/day (measured over a 10 h process lifetime on the Pi); ~130 MiB/day of the remainder is autocheckpoint writeback — SQLite's 1000-page default trips every ~40 min and rewrites the same hot index/interior pages into the main db each time. At 8192 pages (32 MiB at the 4096-byte page size) the cadence drops to ~5 h, cutting those in-place rewrites ~8x, expected total ≈190 MiB/day. The previous SD card died of write wear; the current card's endurance is unknown, which argues for cutting known writes, not against it. Codex approved the decision and this shape (thread 01a0205d).
Decision
PRAGMA wal_autocheckpoint = 8192 on every read-write querylog.db connection. Hardcoded constant, no config knob, no checkpoint task. synchronous=NORMAL and the daily retention wal_checkpoint(TRUNCATE) (queries_repo.zig:169, called from the retention pass) stay as they are. config.db — including the diagnostics store's connection, which app.zig:416 opens via openConfigDb despite the variable name events_db — keeps the SQLite default. There are exactly two database files; nothing named events.db exists.
Durability contract (goes in the constant's comment, stated precisely)
- Commit never fsyncs at
synchronous=NORMAL; the checkpoint's fsync is the only guaranteed durability boundary. This change moves that boundary from ~40 min to ~5 h of querylog data (query rows + upstream-history minutes) under power loss or kernel panic. Typical loss stays far smaller (kernel writeback), but that is not a guarantee. - Process crash or clean stop loses nothing committed, at any threshold. Consistency is never at risk: recovery replays the longest valid WAL prefix atomically.
- 32 MiB is an expectation, not a cap: a pinned reader snapshot stops a passive checkpoint partway and the WAL overshoots until the reader finishes; the daily TRUNCATE is the backstop that shrinks the file.
Implementation
- src/storage/db.zig —
Pragmas(:753) gainswal_autocheckpoint_pages: ?i32 = null.applyPragmas(:762), when non-null:PRAGMA wal_autocheckpoint = N;then read back via the pragma's own return and fail loudly on mismatch — mirror theforeign_keysset-and-verify at :781-783. Default null leaves every existing.{}caller (config.db sites, tests) untouched with zero diffs. db.zig stays generic; it must not know the word querylog. - src/storage/querylog_schema.zig — owns the constant (the module already owns querylog policy: fingerprint, DDL, recreate classification):
pub const wal_autocheckpoint_pages: i32 = 8192;carrying the durability contract above as its comment. Passed at both productionapplyPragmassites: the probe path (:129) andcreateFresh(:253). The thirdapplyPragmasin that file (:273) is inside an in-memory test and stays.{}deliberately. - src/cli.zig —
reopenQuerylogDb(:354) passes the constant. These three sites are the only read-write querylog connections by construction — every open flows throughquerylog_schema.openorDataDir.reopenQuerylogDb.
Tests
- Unit, db.zig, in-memory (the pragma reads back per-connection regardless of journal mode): default
PragmasleavesPRAGMA wal_autocheckpointat 1000; a set value reads back. - File-backed storage integration test through the real helpers:
openQuerylogDbandreopenQuerylogDbconnections both read back 8192; anopenConfigDbconnection reads 1000. - The read-back inside
applyPragmasmakes misapplication loud at startup, complementing both.
Docs
- CHANGELOG Unreleased, Changed: the checkpoint cadence change, the measured why, and the widened power-loss window stated per the durability contract (not as an unconditional bound).
- One short paragraph appended to specs/querylog-batching.md linking here.
Rejected (do not relitigate without new facts)
- Config knob: nobody tunes this twice; scope is small on purpose.
- Periodic checkpoint task: reproduces autocheckpoint with more moving parts (cadence state, busy handling, shutdown, diagnostics).
wal_autocheckpoint=0+ daily TRUNCATE only: unbounded intraday WAL growth under reader pinning; strictly worse.journal_size_limit: redundant with the daily TRUNCATE, and rejecting it needs no claim about passive checkpoints never truncating (after a completed checkpoint resets the WAL, the limit does truncate on the next write — the knob is merely surplus here).- Touching config.db policy or differentiating reader vs writer querylog connections: readers cannot trip checkpoints, the pragma is inert on them; uniformity is simpler.
Gates
zig build testand-Dintegration0 failed; fmt clean.- Field verification on the released build (the Pi deploys releases, not branches, so this necessarily follows the cut — owner-ordered 2026-08-21): confirm the WAL resets normally, writeback falls materially, and no batches drop. What to measure and over what window is the deployment side's call; a bad result reverts the constant in a follow-up patch release.