mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The per-account copies roll one account back on the same server; nothing the app made could rebuild a lost one. A scheduled job now runs pg_dump over the whole database (every schema, pg-boss included) into DB_BACKUP_DIR as healthlog-<UTC time>-daily.dump, with a manifest beside it: app version and build, newest migration, Postgres version, size and SHA-256, exact row counts of every table, and the fingerprints of the keys the database needs. The counts read the same exported snapshot pg_dump reads, so a dump taken while the app writes still matches its manifest. The archive is written as .partial, listed back with pg_restore --list, and renamed after its manifest; a failed run leaves nothing named like a good dump. Retention removes only files this job wrote, oldest first, and never the newest good one (DB_BACKUP_KEEP, default 7; two pre-migration dumps). With the off-host bucket configured, each dump and its manifest go up under database/ in the envelope the account copies use, streamed. Every run lands in the new database_backup_runs ledger (migration 0486), and getDatabaseBackupHealth() answers the last good dump, the last failure, whether the directory is writable and whether off-host is configured. The boot readiness summary says where dumps go, or that none can be kept. The dump, naming, retention and restore rules live in one plain ESM module so the image's own scripts can share them without tsx.
41 lines
1.7 KiB
TypeScript
41 lines
1.7 KiB
TypeScript
/**
|
|
* The queue facts of the nightly whole-database dump: its name, its schedule
|
|
* and its send options. Apart from the job so the worker registry can read
|
|
* them without loading it (`job-registry-graph.test.ts`).
|
|
*/
|
|
import { BACKUP_HEARTBEAT_SECONDS } from "@/lib/jobs/data-backup-policy";
|
|
import { resolveBackupSchedule } from "@/lib/database-backup/dump-core.mjs";
|
|
|
|
export const DATABASE_BACKUP_QUEUE = "database-backup";
|
|
|
|
/**
|
|
* `DB_BACKUP_SCHEDULE`, read once when the worker loads its registry, in the
|
|
* zone every cron runs in (`createAndSchedule`, Europe/Berlin). Default 01:30,
|
|
* an hour before the off-host account copies so the two uploads do not meet.
|
|
* A value that is not a five-field cron keeps the default, and the boot
|
|
* readiness summary says so.
|
|
*/
|
|
export const DATABASE_BACKUP_CRON = resolveBackupSchedule(process.env).cron;
|
|
|
|
/**
|
|
* Four hours. `pg_dump` of a large instance takes minutes, the upload of the
|
|
* dump about as long as the dump; the window is there to be impossible to hit
|
|
* for a working run. The handler kills `pg_dump` at three quarters of it
|
|
* (`jobDeadline`), so a hung dump ends as a failed run with a reason rather
|
|
* than an expired job.
|
|
*/
|
|
export const DATABASE_BACKUP_EXPIRE_SECONDS = 4 * 60 * 60;
|
|
|
|
/**
|
|
* What the nightly schedule sends with: the expiry, the backups' heartbeat,
|
|
* and one retry. A dump that failed for a passing reason (a lock it waited
|
|
* out, a bucket that answered 503) gets a second attempt; one that fails
|
|
* again waits for the next night, and the failure stays on the admin page.
|
|
*/
|
|
export const DATABASE_BACKUP_SEND_OPTIONS = {
|
|
expireInSeconds: DATABASE_BACKUP_EXPIRE_SECONDS,
|
|
heartbeatSeconds: BACKUP_HEARTBEAT_SECONDS,
|
|
retryLimit: 1,
|
|
retryDelay: 15 * 60,
|
|
} as const;
|