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:
Marc-André Bombeck committed 2026-10-20 16:06:10 +02:00
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", () => {