fix(sf-db): write snapshots atomically

This commit is contained in:
Mikael Hugo 2026-05-15 19:49:04 +02:00
parent a8a28bd7c0
commit 15ae3d02b7
2 changed files with 18 additions and 1 deletions

View file

@ -23,6 +23,8 @@ import {
mkdirSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
statSync,
unlinkSync,
writeFileSync,
@ -199,13 +201,25 @@ function createDatabaseSnapshot(rawDb, path) {
if (latest > 0 && Date.now() - latest < DB_BACKUP_MIN_INTERVAL_MS) return;
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupPath = join(dir, `sf.db.${stamp}`);
rawDb.exec(`VACUUM INTO ${sqliteStringLiteral(backupPath)}`);
const tmpPath = `${backupPath}.tmp`;
rmSync(tmpPath, { force: true });
rawDb.exec(`VACUUM INTO ${sqliteStringLiteral(tmpPath)}`);
renameSync(tmpPath, backupPath);
pruneDatabaseBackups(dir);
} catch (err) {
logWarning(
"sf-db",
`database snapshot failed: ${getErrorMessage(err)}`,
);
try {
for (const entry of readdirSync(dir)) {
if (entry.startsWith("sf.db.") && entry.endsWith(".tmp")) {
rmSync(join(dir, entry), { force: true });
}
}
} catch {
// Best-effort cleanup; preserve the original snapshot warning.
}
}
}
function performDatabaseMaintenance(rawDb, path) {

View file

@ -11,6 +11,7 @@ import {
mkdtempSync,
readdirSync,
rmSync,
statSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
@ -453,6 +454,8 @@ test("openDatabase_when_file_backed_creates_db_snapshot_and_maintenance_marker",
name.startsWith("sf.db."),
);
assert.equal(backups.length, 1);
assert.equal(backups[0].endsWith(".tmp"), false);
assert.equal(statSync(join(backupDir, backups[0])).size > 0, true);
assert.equal(existsSync(join(backupDir, "maintenance.json")), true);
});