From c9126f718d2b752147bb663cae6f7075f3eeacbd Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 24 May 2026 21:15:15 -0400 Subject: [PATCH] DB resilience: hourly WAL checkpoint + daily rolling 7-day backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hourly PASSIVE WAL checkpoint prevents unbounded WAL growth and ensures all writes are merged into the main .db file regularly. Previously the WAL was never checkpointed — all data was accumulating in stories.db-wal with no protection if that file was lost. - Daily backup using better-sqlite3 .backup() writes a safe online snapshot to data/backups/stories-YYYY-MM-DD.db on startup and every 24 h; keeps last 7 days, pruning older ones automatically. - busy_timeout = 5000 so concurrent requests wait briefly rather than failing with SQLITE_BUSY. Co-Authored-By: Claude Sonnet 4.6 --- server/db.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/server/db.js b/server/db.js index 1217924..b9a0cdd 100644 --- a/server/db.js +++ b/server/db.js @@ -1,5 +1,5 @@ import Database from 'better-sqlite3' -import { mkdirSync } from 'fs' +import { mkdirSync, readdirSync, unlinkSync } from 'fs' import path from 'path' import { fileURLToPath } from 'url' @@ -10,6 +10,42 @@ mkdirSync(dataDir, { recursive: true }) const db = new Database(path.join(dataDir, 'stories.db')) db.pragma('journal_mode = WAL') db.pragma('foreign_keys = ON') +db.pragma('busy_timeout = 5000') // wait up to 5 s instead of failing immediately + +// Merge the WAL back into the main DB file once an hour. +// Without this, all writes live in the WAL indefinitely, and losing +// that file means losing all data written since the last checkpoint. +setInterval(() => { + try { db.pragma('wal_checkpoint(PASSIVE)') } + catch (err) { console.warn('[db] WAL checkpoint failed:', err.message) } +}, 60 * 60 * 1000) // every hour + +// Daily backup — keeps the last 7 daily snapshots in data/backups/ +const backupDir = path.join(dataDir, 'backups') +mkdirSync(backupDir, { recursive: true }) + +async function runBackup() { + try { + const stamp = new Date().toISOString().slice(0, 10) // YYYY-MM-DD + const dest = path.join(backupDir, `stories-${stamp}.db`) + await db.backup(dest) + console.log(`[db] Backup written → ${dest}`) + + // Prune backups older than 7 days + const files = readdirSync(backupDir) + .filter(f => f.startsWith('stories-') && f.endsWith('.db')) + .sort() + for (const old of files.slice(0, -7)) { + try { unlinkSync(path.join(backupDir, old)) } catch {} + } + } catch (err) { + console.warn('[db] Backup failed:', err.message) + } +} + +// Run once on startup, then every 24 h +runBackup() +setInterval(runBackup, 24 * 60 * 60 * 1000) db.exec(` CREATE TABLE IF NOT EXISTS users (