Files
nxdns/specs/querylog-autocheckpoint.md
T

5.1 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

  1. src/storage/db.zigPragmas (:753) gains wal_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 the foreign_keys set-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.
  2. 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 production applyPragmas sites: the probe path (:129) and createFresh (:253). The third applyPragmas in that file (:273) is inside an in-memory test and stays .{} deliberately.
  3. src/cli.zigreopenQuerylogDb (:354) passes the constant. These three sites are the only read-write querylog connections by construction — every open flows through querylog_schema.open or DataDir.reopenQuerylogDb.

Tests

  • Unit, db.zig, in-memory (the pragma reads back per-connection regardless of journal mode): default Pragmas leaves PRAGMA wal_autocheckpoint at 1000; a set value reads back.
  • File-backed storage integration test through the real helpers: openQuerylogDb and reopenQuerylogDb connections both read back 8192; an openConfigDb connection reads 1000.
  • The read-back inside applyPragmas makes 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

  1. zig build test and -Dintegration 0 failed; fmt clean.
  2. Field verification on the released build (the Pi deploys releases, not branches, so this necessarily follows the cut — owner-ordered 2026-08-21): one 24-hour run spanning several autocheckpoints and a retention pass — WAL resets normally, writeback falls materially, no dropped batches; measured as process write_bytes AND device sectors (/sys/block/mmcblk0/stat). The rpi-nixos-iac session runs it; a bad result reverts the constant in a follow-up patch release.