DB resilience: hourly WAL checkpoint + daily rolling 7-day backup
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
c6d1554215
commit
c9126f718d
38
server/db.js
38
server/db.js
@ -1,5 +1,5 @@
|
|||||||
import Database from 'better-sqlite3'
|
import Database from 'better-sqlite3'
|
||||||
import { mkdirSync } from 'fs'
|
import { mkdirSync, readdirSync, unlinkSync } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
@ -10,6 +10,42 @@ mkdirSync(dataDir, { recursive: true })
|
|||||||
const db = new Database(path.join(dataDir, 'stories.db'))
|
const db = new Database(path.join(dataDir, 'stories.db'))
|
||||||
db.pragma('journal_mode = WAL')
|
db.pragma('journal_mode = WAL')
|
||||||
db.pragma('foreign_keys = ON')
|
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(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user