fix(migrations): retry a marked migration that lost a deadlock

A marked migration's DO block holds the locks it takes until it commits,
so an app transaction taking two of its tables in the other order can
deadlock with it. The block then rolled back whole, exactly as on a lock
timeout, so the boot retries 40P01 for marked migrations as it does 55P03.
Corrects 0415's note that the drops hold their lock for an instant.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 16:06:10 +02:00
1 parent 2b81b4f23e
commit 408ae35b7c
4 files changed
+108 -22

No files matched your search

+58 -2
View File
@@ -47,6 +47,11 @@ const P3018_UNIQUE = P3018_LOCK.replace("55P03", "23505").replace(
'duplicate key value violates unique constraint "x"',
);
const P3018_DEADLOCK = P3018_LOCK.replace("55P03", "40P01").replace(
"canceling statement due to lock timeout",
"deadlock detected",
);
const P3009 = `Error: P3009
migrate found failed migrations in the target database, new migrations will not be applied. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
@@ -78,7 +83,11 @@ describe("which failure is retried", () => {
failedRows: null,
readMigration: reader(MARKED),
}),
).toEqual({ retry: true, migration: "0412_measurement_hot_updates" });
).toEqual({
retry: true,
migration: "0412_measurement_hot_updates",
code: "55P03",
});
});
it("does not retry a lock timeout of an unmarked migration", () => {
@@ -93,6 +102,31 @@ describe("which failure is retried", () => {
});
});
it("retries a deadlock of a marked migration, which rolled back whole", () => {
expect(lockTimeoutInReport(P3018_DEADLOCK)).toBe(
"0412_measurement_hot_updates",
);
expect(
decideLockRetry({
output: P3018_DEADLOCK,
failedRows: null,
readMigration: reader(MARKED),
}),
).toEqual({
retry: true,
migration: "0412_measurement_hot_updates",
code: "40P01",
});
// Unmarked, a deadlock stays fatal like a lock timeout.
expect(
decideLockRetry({
output: P3018_DEADLOCK,
failedRows: null,
readMigration: reader(UNMARKED),
}).retry,
).toBe(false);
});
it("does not retry any other database error", () => {
expect(
decideLockRetry({
@@ -123,7 +157,11 @@ describe("which failure is retried", () => {
failedRows,
readMigration: reader(MARKED),
}),
).toEqual({ retry: true, migration: "0412_measurement_hot_updates" });
).toEqual({
retry: true,
migration: "0412_measurement_hot_updates",
code: "55P03",
});
// A stored failure of another kind stays fatal.
expect(
decideLockRetry({
@@ -295,6 +333,24 @@ process.exit(0);
expect(result.stdout).toContain("timed out waiting for a lock (55P03)");
});
it("does the same for a deadlock, and names it", () => {
const { run, calls } = setup(
[
[1, P3018_DEADLOCK],
[0, "All migrations have been successfully applied."],
],
MARKED,
);
const result = run();
expect(result.status).toBe(0);
expect(calls()).toEqual([
"migrate deploy --schema x",
"migrate resolve --rolled-back 0412_measurement_hot_updates --schema x",
"migrate deploy --schema x",
]);
expect(result.stdout).toContain("was cancelled in a deadlock (40P01)");
});
it("gives up after the last delay with Prisma's exit code", () => {
const { run, calls } = setup(
[