mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(db-backup): take the pre-migration dump when pending migrations cannot be counted
Any error counting pending migrations (a connect or statement timeout, a database still starting up) exited the step with "no dump before migrating", and the migration that ran a second later went ahead without its way back. An unknown count now takes the dump; a database that cannot be reached at all then fails the dump, and the boot stops naming the switch that skips it.
This commit is contained in:
1 parent
6f83e7e685
commit
756885eec0
2 files changed
+87
-8
No files matched your search
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* The pre-migration dump decides whether to take a dump from the count of
|
||||
* pending migrations. A count that fails (a connect or statement timeout, a
|
||||
* database still starting up) used to read as "nothing pending": the step
|
||||
* exited 0 with no dump, and the migration a second later ran without its
|
||||
* way back. "Cannot tell" now takes the dump.
|
||||
*
|
||||
* The database is a stand-in: `createDatabaseDump` is a spy, `connect`
|
||||
* answers the two facts the step reads (an applied migration, some tables).
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const pendingMigrations = vi.fn<() => Promise<string[]>>();
|
||||
const createDatabaseDump = vi.fn(async () => ({
|
||||
dumpPath: "/b/healthlog-20261025T013000Z-pre-migration.dump",
|
||||
manifest: { dump: { bytes: 1024 }, rowCounts: { "public.users": 1 } },
|
||||
}));
|
||||
|
||||
vi.mock("../pending-migrations.mjs", () => ({
|
||||
pendingMigrations: () => pendingMigrations(),
|
||||
}));
|
||||
vi.mock("../../src/lib/database-backup/dump-core.mjs", async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
connect: async () => ({
|
||||
query: async () => ({ rows: [] }),
|
||||
end: async () => {},
|
||||
}),
|
||||
latestMigration: async () => "0488_account_deletion_grace",
|
||||
listUserTables: async () => ["public.users"],
|
||||
createDatabaseDump: () => createDatabaseDump(),
|
||||
applyRetention: async () => [],
|
||||
}));
|
||||
|
||||
import { preMigrationDump } from "../db-dump.mjs";
|
||||
|
||||
const env = {
|
||||
DATABASE_URL: "postgresql://u:p@db:5432/healthlog",
|
||||
DB_BACKUP_DIR: "/b",
|
||||
};
|
||||
const kept = () =>
|
||||
({ writable: true, persistent: true, reason: "volume" }) as const;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
createDatabaseDump.mockClear();
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("the dump before migrating", () => {
|
||||
it("takes the dump when the pending migrations cannot be counted", async () => {
|
||||
pendingMigrations.mockRejectedValue(
|
||||
new Error("the database system is starting up"),
|
||||
);
|
||||
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
|
||||
expect(createDatabaseDump).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("takes none when nothing is pending", async () => {
|
||||
pendingMigrations.mockResolvedValue([]);
|
||||
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
|
||||
expect(createDatabaseDump).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("takes one before pending migrations", async () => {
|
||||
pendingMigrations.mockResolvedValue(["0489_database_backup_run_key"]);
|
||||
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
|
||||
expect(createDatabaseDump).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+17
-8
@@ -12,7 +12,9 @@
|
||||
* newest two, neither older than the oldest nightly dump except the newest.
|
||||
*
|
||||
* Exit 0: the dump was written, or there was nothing to do (no pending
|
||||
* migration, a new database, `DB_BACKUP_BEFORE_MIGRATE=false`), or there is no
|
||||
* migration, a new database, `DB_BACKUP_BEFORE_MIGRATE=false`; a count of
|
||||
* pending migrations that fails is not "none", and the dump is taken), or
|
||||
* there is no
|
||||
* directory to put it in that survives the container (none writable, or only
|
||||
* the image's own directory without the volume, which the update that brings
|
||||
* these migrations is about to throw away), which is said as a warning and is
|
||||
@@ -74,6 +76,11 @@ export async function preMigrationDump(env = process.env, options = {}) {
|
||||
const databaseUrl = env.DATABASE_URL;
|
||||
if (!databaseUrl) return 0;
|
||||
|
||||
// "Cannot tell" is not "nothing to do": a count that timed out on a
|
||||
// database still starting up was read as no pending migration, and the
|
||||
// migration a second later ran without its way back. Unknown, the dump is
|
||||
// taken.
|
||||
/** @type {string[] | null} */
|
||||
let pending;
|
||||
try {
|
||||
pending = await pendingMigrations({
|
||||
@@ -82,11 +89,15 @@ export async function preMigrationDump(env = process.env, options = {}) {
|
||||
});
|
||||
} catch (error) {
|
||||
warn(
|
||||
`could not count pending migrations (${error instanceof Error ? error.message : String(error)}); no dump before migrating.`,
|
||||
`could not count pending migrations (${error instanceof Error ? error.message : String(error)}); taking the dump before migrating anyway.`,
|
||||
);
|
||||
return 0;
|
||||
pending = null;
|
||||
}
|
||||
if (pending.length === 0) return 0;
|
||||
if (pending !== null && pending.length === 0) return 0;
|
||||
const migrations =
|
||||
pending === null
|
||||
? "any pending migrations"
|
||||
: `${pending.length} pending migration(s)`;
|
||||
|
||||
// A database no migration has touched holds nothing to lose.
|
||||
const client = await connect(databaseUrl);
|
||||
@@ -115,15 +126,13 @@ export async function preMigrationDump(env = process.env, options = {}) {
|
||||
? "is in memory (tmpfs) and would not keep a dump past a restart"
|
||||
: "is inside the container, not on a mounted volume, and would not keep a dump past this update";
|
||||
warn(
|
||||
`${dir} ${why}, so no dump is taken before the ${pending.length} pending migration(s). ` +
|
||||
`${dir} ${why}, so no dump is taken before ${pending === null ? migrations : `the ${migrations}`}. ` +
|
||||
"Mount the backups volume (docker-compose.yml) to keep a way back; continuing.",
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
log(
|
||||
`taking a database dump before ${pending.length} pending migration(s) into ${dir}...`,
|
||||
);
|
||||
log(`taking a database dump before ${migrations} into ${dir}...`);
|
||||
const started = Date.now();
|
||||
try {
|
||||
const { dumpPath, manifest } = await createDatabaseDump({
|
||||
|
||||
Reference in new issue
Block a user