Run scheduled backups in an isolated subprocess

This commit is contained in:
syntaxbullet
2026-09-08 16:12:53 +02:00
parent 31e4f3b2ef
commit b0950cb4b4
4 changed files with 98 additions and 14 deletions

View File

@@ -82,18 +82,40 @@ export function restoreBackup(source: string, destination: string) {
} finally { if (existsSync(temporary)) unlinkSync(temporary); }
return destination;
}
export function startBackups(database: Database, config: BackupConfig) {
const state = { enabled: config.enabled, lastSuccess: 0, failed: false };
if (!config.enabled) return { state, stop() {} };
const tick = () => {
try {
const latest = latestBackupTime(config);
if (latest && Date.now() - latest < config.intervalMs) { state.lastSuccess = latest; state.failed = false; return; }
createBackup(database, config); state.lastSuccess = Date.now(); state.failed = false;
console.log(JSON.stringify({ event: "backup_completed", timestamp: new Date().toISOString() }));
} catch { state.failed = true; console.error(JSON.stringify({ event: "backup_failed", timestamp: new Date().toISOString() })); }
};
tick();
const timer = setInterval(tick, 60000); timer.unref();
return { state, stop: () => clearInterval(timer) };
/** Runs all filesystem and SQLite backup work in a separate Bun process. */
export async function runScheduledBackup(database: string, config: BackupConfig): Promise<number> {
const source = new URL("../../scripts/scheduled-backup.ts", import.meta.url);
const built = new URL("../scripts/scheduled-backup.ts", import.meta.url);
const script = (existsSync(source) ? source : built).pathname;
const child = Bun.spawn([process.execPath, script, resolve(database), JSON.stringify(config)], {
stdout: "pipe", stderr: "ignore",
});
const [output, status] = await Promise.all([new Response(child.stdout).text(), child.exited]);
if (status !== 0) throw new Error("Scheduled backup process failed");
const { timestamp, created } = JSON.parse(output) as { timestamp: number; created: boolean };
if (!Number.isFinite(timestamp) || timestamp <= 0) throw new Error("Scheduled backup returned an invalid timestamp");
if (created) console.log(JSON.stringify({ event: "backup_completed", timestamp: new Date(timestamp).toISOString() }));
return timestamp;
}
export function startBackups(database: Database | string, config: BackupConfig, run = runScheduledBackup) {
const state = { enabled: config.enabled, lastSuccess: 0, failed: false };
let running: Promise<void> | undefined;
let stopped = false;
const tick = (): Promise<void> => {
if (stopped || !config.enabled) return Promise.resolve();
if (running) return running;
running = (async () => {
try {
const path = typeof database === 'string' ? database : database.filename;
if (!path || path === ':memory:') throw new Error('Scheduled backups require a file database');
state.lastSuccess = await run(path, config); state.failed = false;
} catch { state.failed = true; console.error(JSON.stringify({ event: "backup_failed", timestamp: new Date().toISOString() })); }
})().finally(() => { running = undefined; });
return running;
};
// Callers await readiness before accepting traffic, matching the original startup check.
const ready = tick();
const timer = config.enabled ? setInterval(() => { void tick(); }, 60000) : undefined;
timer?.unref();
return { state, ready, tick, stop: () => { stopped = true; clearInterval(timer); } };
}