feat(backup): scheduled snapshot worker wired into server lifecycle
This commit is contained in:
@@ -26,6 +26,7 @@ import { adminRoutes } from './routes/admin.js';
|
|||||||
import { gifRoutes } from './routes/gif.js';
|
import { gifRoutes } from './routes/gif.js';
|
||||||
import { federationRoutes } from './routes/federation.js';
|
import { federationRoutes } from './routes/federation.js';
|
||||||
import { startFederationWorkers, stopFederationWorkers } from './utils/federationWorker.js';
|
import { startFederationWorkers, stopFederationWorkers } from './utils/federationWorker.js';
|
||||||
|
import { startBackupWorker, stopBackupWorker } from './utils/backupWorker.js';
|
||||||
import './utils/federationRollback.js'; // Side-effect: registers rollback callbacks for outbox terminal failures.
|
import './utils/federationRollback.js'; // Side-effect: registers rollback callbacks for outbox terminal failures.
|
||||||
import { registerCallRelayHooks } from './ws/events.js';
|
import { registerCallRelayHooks } from './ws/events.js';
|
||||||
import { resetStalePresenceOnBoot } from './utils/presenceBoot.js';
|
import { resetStalePresenceOnBoot } from './utils/presenceBoot.js';
|
||||||
@@ -177,9 +178,12 @@ async function main(): Promise<void> {
|
|||||||
startFederationWorkers();
|
startFederationWorkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startBackupWorker();
|
||||||
|
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
console.log('Shutting down...');
|
console.log('Shutting down...');
|
||||||
stopFederationWorkers();
|
stopFederationWorkers();
|
||||||
|
stopBackupWorker();
|
||||||
await app.close();
|
await app.close();
|
||||||
closeDatabase(); // checkpoints WAL — leaves a complete on-disk file
|
closeDatabase(); // checkpoints WAL — leaves a complete on-disk file
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
const { createSnapshot, pruneSnapshots, state } = vi.hoisted(() => ({
|
||||||
|
createSnapshot: vi.fn(() => '/tmp/snap-scheduled.db'),
|
||||||
|
pruneSnapshots: vi.fn(),
|
||||||
|
state: { disabled: false },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./backup.js', () => ({ createSnapshot, pruneSnapshots }));
|
||||||
|
vi.mock('../db/index.js', () => ({ getRawDb: () => ({}) }));
|
||||||
|
vi.mock('../config.js', () => ({
|
||||||
|
config: { backup: { get disabled() { return state.disabled; }, intervalHours: 1 } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { startBackupWorker, stopBackupWorker } from './backupWorker.js';
|
||||||
|
|
||||||
|
beforeEach(() => { vi.useFakeTimers(); createSnapshot.mockClear(); pruneSnapshots.mockClear(); state.disabled = false; });
|
||||||
|
afterEach(() => { stopBackupWorker(); vi.useRealTimers(); });
|
||||||
|
|
||||||
|
describe('backupWorker', () => {
|
||||||
|
it('snapshots on each interval tick', () => {
|
||||||
|
startBackupWorker();
|
||||||
|
vi.advanceTimersByTime(60 * 60 * 1000); // 1h
|
||||||
|
expect(createSnapshot).toHaveBeenCalledWith(expect.anything(), 'scheduled');
|
||||||
|
expect(pruneSnapshots).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when disabled', () => {
|
||||||
|
state.disabled = true;
|
||||||
|
startBackupWorker();
|
||||||
|
vi.advanceTimersByTime(60 * 60 * 1000);
|
||||||
|
expect(createSnapshot).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { config } from '../config.js';
|
||||||
|
import { getRawDb } from '../db/index.js';
|
||||||
|
import { createSnapshot, pruneSnapshots } from './backup.js';
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
export function startBackupWorker(): void {
|
||||||
|
if (config.backup.disabled) {
|
||||||
|
console.log('[backup] scheduled worker disabled via BACKUP_DISABLED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (timer) return;
|
||||||
|
const intervalMs = config.backup.intervalHours * 60 * 60 * 1000;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
try {
|
||||||
|
const snap = createSnapshot(getRawDb(), 'scheduled');
|
||||||
|
pruneSnapshots();
|
||||||
|
console.log(`[backup] scheduled snapshot written: ${snap}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[backup] scheduled snapshot failed: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
// Do not keep the event loop alive solely for backups.
|
||||||
|
if (typeof timer.unref === 'function') timer.unref();
|
||||||
|
console.log(`[backup] scheduled worker started (every ${config.backup.intervalHours}h)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopBackupWorker(): void {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user