From bf9ad164861d5e060e90ad7f960350a6f68e4966 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Thu, 6 Aug 2026 22:12:01 -0500 Subject: [PATCH] The checkpointer startup line no longer describes a policy it stopped having MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It still read "escalate >16MB or 3 growing runs" after #240 added a size floor and a cooldown to that second rule. Growth-across-three-runs on its own is exactly the half that no longer holds, so the line described a checkpointer that does not exist — and it is the line an operator reads to learn the policy. During an incident it would send you hunting for a blocking checkpoint that the new gates had in fact suppressed. [wal-checkpoint] off-thread checkpointer started (PASSIVE every 15000ms; blocking TRUNCATE when the WAL exceeds 16MB, or after 3 growing runs but only at >=8MB and at most once per 300s; respawn max 5/60000ms) A test now asserts the line reports every knob that governs the decision, since nothing else keeps a log string and the rule it describes in step. Writing it caught its own bug first: anchoring the slice back to `return worker;` matched the idempotence guard at the top of startWalCheckpointer(), not the log below it, so the window was empty and every assertion passed vacuously. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS --- server/db/wal-checkpointer.js | 12 +++++++++- .../wal-checkpoint-starvation-floor.test.js | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/db/wal-checkpointer.js b/server/db/wal-checkpointer.js index 78f8462..24b08a7 100644 --- a/server/db/wal-checkpointer.js +++ b/server/db/wal-checkpointer.js @@ -121,7 +121,17 @@ function startWalCheckpointer(db, dbPath) { try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch (_) { /* best-effort */ } worker = spawnWorker(); - console.log(`[wal-checkpoint] off-thread checkpointer started (every ${config.walCheckpointIntervalMs}ms; escalate >${config.walCheckpointHighWaterMB}MB or ${config.walCheckpointStarvationRuns} growing runs; respawn max ${config.walCheckpointRespawnMax}/${config.walCheckpointRespawnWindowMs}ms)`); + // #240: this line is where an operator learns the escalation policy, so it must state ALL of + // it. It advertised only "3 growing runs" after the size floor and the cooldown were added, + // which is the half that no longer holds on its own — and reading it during an incident would + // send you looking for a checkpoint that the gates had in fact suppressed. + console.log( + `[wal-checkpoint] off-thread checkpointer started (PASSIVE every ${config.walCheckpointIntervalMs}ms; ` + + `blocking TRUNCATE when the WAL exceeds ${config.walCheckpointHighWaterMB}MB, ` + + `or after ${config.walCheckpointStarvationRuns} growing runs but only at >=${config.walCheckpointStarvationFloorMB}MB ` + + `and at most once per ${Math.round(config.walCheckpointEscalateCooldownMs / 1000)}s; ` + + `respawn max ${config.walCheckpointRespawnMax}/${config.walCheckpointRespawnWindowMs}ms)` + ); return worker; } diff --git a/server/test/wal-checkpoint-starvation-floor.test.js b/server/test/wal-checkpoint-starvation-floor.test.js index a0f2bba..eff15ba 100644 --- a/server/test/wal-checkpoint-starvation-floor.test.js +++ b/server/test/wal-checkpoint-starvation-floor.test.js @@ -108,6 +108,28 @@ test('#240: the worker actually applies the floor it is handed', () => { // Measured, not assumed: with a single reader mid-transaction, TRUNCATE returns busy=1 // after sitting on its 5s busy timeout and reclaims nothing (probe: WAL 8.8MB -> 8.8MB, // worst main-thread write 4,936ms). That outcome must not be logged as a success. +// The startup line is where an operator learns the policy. It went stale the moment the gates were +// added — it still promised "3 growing runs" with no mention of the floor or the cooldown, so it +// described a checkpointer that no longer existed. A log line that states a rule has to state the +// whole rule, and nothing but a test keeps the two in step. +test('#240: the startup line states the WHOLE escalation policy', () => { + const ctl = fs.readFileSync(path.join(__dirname, '..', 'db', 'wal-checkpointer.js'), 'utf8'); + // Anchor forward from the message text, not back to `return worker;` — the idempotence guard at + // the top of startWalCheckpointer() returns first, so slicing to it yields nothing at all. + const start = ctl.indexOf('off-thread checkpointer started'); + assert.ok(start > 0, 'the startup line is gone entirely'); + const line = ctl.slice(start, start + 800); + for (const knob of [ + 'walCheckpointIntervalMs', + 'walCheckpointHighWaterMB', + 'walCheckpointStarvationRuns', + 'walCheckpointStarvationFloorMB', + 'walCheckpointEscalateCooldownMs', + ]) { + assert.ok(line.includes(knob), `the startup line must report ${knob} — an operator reads it as the policy`); + } +}); + test('#240: a TRUNCATE that reclaimed nothing says so', () => { const src = fs.readFileSync(path.join(__dirname, '..', 'db', 'wal-checkpointer-worker.js'), 'utf8'); assert.match(src, /busy === 1/, 'worker must inspect the checkpoint result');