mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(migrations): retire the measurements prefix index without waiting at boot
0416 ran DROP INDEX CONCURRENTLY at boot, which waits without bound for every transaction on the table; a pg_dump or a backfill outlasted the start period and the next boot stopped at P3009. The migration now tries a plain drop with a two-second lock wait and tolerates the timeout, and the nightly measurement maintenance drops what is left concurrently under a bounded wait, retrying the next night. A fresh or shadow database takes the lock at once, so the history still ends where the schema does. The guard refuses a concurrent drop in a migration and ties every retired index to a tolerant one.
This commit is contained in:
1 parent
a275a5bb65
commit
2b81b4f23e
5 files changed
+388
-19
No files matched your search
@@ -23,6 +23,8 @@ import { readdirSync, readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { RETIRED_MEASUREMENT_INDEXES } from "@/lib/jobs/measurement-maintenance";
|
||||
|
||||
import { RETRY_MARKER } from "../../scripts/migrate-deploy.mjs";
|
||||
|
||||
const MIGRATIONS_DIR = join(process.cwd(), "prisma", "migrations");
|
||||
@@ -114,13 +116,14 @@ describe("concurrent index statements", () => {
|
||||
expect(found, found.join("\n")).toEqual([]);
|
||||
});
|
||||
|
||||
it("sees the concurrent migrations there are", () => {
|
||||
const concurrent = migrations()
|
||||
.filter(({ sql }) =>
|
||||
/\bCONCURRENTLY\b/i.test(topLevelStatements(sql).join(";")),
|
||||
)
|
||||
.map(({ name }) => name);
|
||||
expect(concurrent).toContain("0416_drop_measurements_type_prefix_index");
|
||||
it("reads the statements there are", () => {
|
||||
// No migration holds a concurrent statement since 0416 stopped dropping
|
||||
// concurrently; the rule stands for the next one. A reader that stopped
|
||||
// splitting statements would see none and pass, so it must see many.
|
||||
const statements = migrations().flatMap(({ sql }) =>
|
||||
topLevelStatements(sql),
|
||||
);
|
||||
expect(statements.length).toBeGreaterThan(1000);
|
||||
});
|
||||
|
||||
it("refuses a concurrent statement beside another one", () => {
|
||||
@@ -139,6 +142,58 @@ describe("concurrent index statements", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* `DROP INDEX CONCURRENTLY` blocks no reader or writer, but it waits without
|
||||
* bound for every transaction on the table, and in a migration that wait is
|
||||
* the boot's: a pg_dump or a backfill outlasts the container's start period,
|
||||
* the half-applied migration is left on record, and the next boot stops at
|
||||
* P3009. An index is retired instead by a bounded `DROP INDEX` that tolerates
|
||||
* the timeout, finished by the nightly measurement maintenance (0416).
|
||||
*/
|
||||
function concurrentDropViolations(name: string, sql: string): string[] {
|
||||
return topLevelStatements(sql).some((statement) =>
|
||||
/^DROP INDEX CONCURRENTLY\b/i.test(statement),
|
||||
)
|
||||
? [`${name}: DROP INDEX CONCURRENTLY waits on the table at boot`]
|
||||
: [];
|
||||
}
|
||||
|
||||
describe("retired indexes", () => {
|
||||
it("are never dropped concurrently in the boot path", () => {
|
||||
const found = migrations().flatMap(({ name, sql }) =>
|
||||
concurrentDropViolations(name, sql),
|
||||
);
|
||||
expect(found, found.join("\n")).toEqual([]);
|
||||
});
|
||||
|
||||
it("are each dropped by a migration that bounds its wait and tolerates the timeout", () => {
|
||||
expect(RETIRED_MEASUREMENT_INDEXES.length).toBeGreaterThan(0);
|
||||
for (const index of RETIRED_MEASUREMENT_INDEXES) {
|
||||
const owner = migrations().find(({ sql }) =>
|
||||
new RegExp(`DROP INDEX IF EXISTS "${index}"`).test(sql),
|
||||
);
|
||||
expect(owner, index).toBeDefined();
|
||||
expect(owner?.sql).toMatch(/set_config\(\s*'lock_timeout'/);
|
||||
expect(owner?.sql).toMatch(/EXCEPTION WHEN lock_not_available THEN/);
|
||||
}
|
||||
});
|
||||
|
||||
it("counter-check: refuses a concurrent drop, not one in a comment", () => {
|
||||
expect(
|
||||
concurrentDropViolations(
|
||||
"a",
|
||||
'DROP INDEX CONCURRENTLY IF EXISTS "measurements_x_idx";',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
concurrentDropViolations(
|
||||
"b",
|
||||
'-- DROP INDEX CONCURRENTLY "x";\nDROP INDEX IF EXISTS "x";',
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migration lock waits", () => {
|
||||
it("bounds lock waits where they take effect and marks only repeatable files", () => {
|
||||
const found = migrations().flatMap(({ name, sql }) =>
|
||||
@@ -154,6 +209,7 @@ describe("migration lock waits", () => {
|
||||
expect(marked).toContain("0412_measurement_hot_updates");
|
||||
expect(marked).toContain("0414_drop_medication_compliance_rollups");
|
||||
expect(marked).toContain("0415_foreign_key_and_prefix_indexes");
|
||||
expect(marked).toContain("0416_drop_measurements_type_prefix_index");
|
||||
});
|
||||
|
||||
it("catches each shape it exists to refuse", () => {
|
||||
|
||||
Reference in new issue
Block a user