fix: prevent SQLite corruption by checkpointing WAL and closing DB on shutdown

The database was never properly closed on SIGINT/SIGTERM — WAL files were left
in a partially-written state causing "database disk image is malformed" errors.
Adds WAL checkpoint on close, synchronous=NORMAL pragma, SIGTERM handlers, and
a store.close() method that flushes all pending writes before exit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-03-31 23:15:30 -07:00
parent 32fbca551e
commit 38f7cec604
4 changed files with 55 additions and 5 deletions

View File

@@ -650,18 +650,31 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
cronRunner.stop();
notifier.stop();
if (mergeRetryTimer) clearTimeout(mergeRetryTimer);
store.stopWatching();
store.close();
process.exit(0);
});
process.on("SIGTERM", () => {
stuckTaskDetector.stop();
triage.stop();
scheduler.stop();
cronRunner.stop();
notifier.stop();
if (mergeRetryTimer) clearTimeout(mergeRetryTimer);
store.close();
process.exit(0);
});
}
// Dev mode: simplified SIGINT handler (no engine components)
// Dev mode: simplified shutdown handlers (no engine components)
if (opts.dev) {
process.on("SIGINT", () => {
const devShutdown = () => {
notifier.stop();
store.stopWatching();
store.close();
process.exit(0);
});
};
process.on("SIGINT", devShutdown);
process.on("SIGTERM", devShutdown);
}
const server = app.listen(selectedPort);

View File

@@ -122,6 +122,8 @@ export class CentralDatabase {
// Enable WAL mode for concurrent reader/writer access
this.db.exec("PRAGMA journal_mode = WAL");
// Ensure data reaches disk on commit (NORMAL is safe with WAL mode)
this.db.exec("PRAGMA synchronous = NORMAL");
// Enable foreign key enforcement
this.db.exec("PRAGMA foreign_keys = ON");
}
@@ -144,8 +146,14 @@ export class CentralDatabase {
/**
* Close the database connection.
* Checkpoints the WAL first to ensure all writes are flushed to the main db file.
*/
close(): void {
try {
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {
// Best-effort: checkpoint failure is non-fatal
}
this.db.close();
}

View File

@@ -264,6 +264,8 @@ export class Database {
// Enable WAL mode for concurrent reader/writer access
this.db.exec("PRAGMA journal_mode = WAL");
// Ensure data reaches disk on commit (NORMAL is safe with WAL mode)
this.db.exec("PRAGMA synchronous = NORMAL");
// Enable foreign key enforcement
this.db.exec("PRAGMA foreign_keys = ON");
}
@@ -368,10 +370,25 @@ export class Database {
}
}
/**
* Checkpoint the WAL file back into the main database.
* This reduces the risk of corruption from incomplete WAL writes
* and keeps the WAL file from growing unbounded.
*/
checkpoint(): void {
try {
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {
// Best-effort: checkpoint failure is non-fatal
}
}
/**
* Close the database connection.
* Checkpoints the WAL first to ensure all writes are flushed to the main db file.
*/
close(): void {
this.checkpoint();
this.db.close();
}

View File

@@ -1693,6 +1693,18 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
this.recentlyWritten.clear();
}
/**
* Gracefully shut down: stop watching and close the database connection.
* Ensures WAL is checkpointed so no pending writes are lost.
*/
close(): void {
this.stopWatching();
if (this._db) {
this._db.close();
this._db = null;
}
}
/**
* Mark a file path as recently written by an in-process mutation
* so the watcher will skip it.