From 377d678a205b47089a66bac6e79a557188db1dd5 Mon Sep 17 00:00:00 2001 From: Bl0ck154 <36800583+Bl0ck154@users.noreply.github.com> Date: Sun, 20 Sep 2026 08:39:43 +0300 Subject: [PATCH] fix(db): avoid live WAL truncate after incremental reclaim --- src/lib/db/reclaimFreedPages.ts | 17 +++++++++-------- .../unit/db/cleanup-reclaim-freed-pages.test.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/lib/db/reclaimFreedPages.ts b/src/lib/db/reclaimFreedPages.ts index 037eb19ed22..505c786b18a 100644 --- a/src/lib/db/reclaimFreedPages.ts +++ b/src/lib/db/reclaimFreedPages.ts @@ -21,10 +21,11 @@ * the file. We record the request with `vacuumScheduler` and let it run in * its configured window (or never, if the operator said so). * - * In WAL mode the truncation only reaches the main file at a checkpoint, so a - * pass folds the WAL back periodically and ends with `wal_checkpoint(TRUNCATE)`; - * otherwise the `.sqlite` would keep its size and the `-wal` would sit at its - * high-water mark until the next 6-hourly checkpoint. + * In WAL mode reclaimed pages only reach the main file at a checkpoint. A pass + * folds the WAL back periodically and finishes with a non-blocking PASSIVE + * checkpoint. Never TRUNCATE a live WAL: other mapped SQLite handles can be + * invalidated (SIGBUS; #14005). WAL high-water sizing is handled separately by + * WAL maintenance. * * @module lib/db/reclaimFreedPages */ @@ -116,12 +117,12 @@ function isBusyError(err: unknown): boolean { return /database is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(message); } -function checkpointQuietly(db: SqliteAdapter, mode: "PASSIVE" | "TRUNCATE"): void { +function checkpointQuietly(db: SqliteAdapter, mode: "PASSIVE"): void { if (!db.open) return; try { db.checkpoint(mode); } catch { - // Best effort: the 6-hourly TRUNCATE checkpoint in core.ts will catch up. + // Best effort: WAL maintenance will retry later. } } @@ -228,8 +229,8 @@ export async function reclaimFreedPages( } } - // In WAL mode the file only shrinks when the truncating commit is checkpointed. - if (batches > 0) checkpointQuietly(db, "TRUNCATE"); + // Fold reclaimed pages back without mutating live WAL file geometry. + if (batches > 0) checkpointQuietly(db, "PASSIVE"); return finish("incremental", freelist, batches, stopReason, error); } diff --git a/tests/unit/db/cleanup-reclaim-freed-pages.test.ts b/tests/unit/db/cleanup-reclaim-freed-pages.test.ts index 1da61966ec6..4f3b94b3014 100644 --- a/tests/unit/db/cleanup-reclaim-freed-pages.test.ts +++ b/tests/unit/db/cleanup-reclaim-freed-pages.test.ts @@ -115,7 +115,7 @@ test("INCREMENTAL: freed pages are reclaimed in batches, pausing between them", pauses.every((ms) => Number.isFinite(ms) && ms >= 0), "pause receives the last batch's duration" ); - // The pass ends with a TRUNCATE checkpoint, so the truncation reached the main file. + // The pass ends with a PASSIVE checkpoint, so reclaimed pages can reach the main file without a live TRUNCATE. assert.ok( pragmaNumber("page_count") < pageCountBefore, `page_count should shrink (${pageCountBefore} -> ${pragmaNumber("page_count")})`