test(integration): retry a truncate that lost a deadlock

The suite wipes the database between files with one TRUNCATE, which needs
the strictest lock on every table at the same time. The server writes in
thirty-nine places without awaiting the result, so one of those can land
after the test that caused it has returned, and Postgres then picks the
truncate as the deadlock victim. It failed an unrelated file twice in two
days and passed on the rerun both times.

The truncate is retried up to four times, on that one error and no other,
with a short back-off. Three cases hold it there: a deadlock that clears,
an error that is not a deadlock and must not be retried at all, and a
deadlock that never clears and must still fail. All three go red without
the retry.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 12:38:31 +02:00
1 parent 655eafa757
commit 42b9aefdfd
2 files changed
+86 -3

No files matched your search

+23 -3
View File
@@ -101,9 +101,29 @@ export async function truncateAllTables(client: PrismaClient): Promise<void> {
}
const quoted = tables.map((t) => `"${t}"`).join(", ");
await client.$executeRawUnsafe(
`TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE;`,
);
// A truncate takes the strictest lock there is on every table at once, so it
// loses to anything still holding a row lock. Thirty-nine places in the
// server write without awaiting the result (a last-used stamp, a delivery
// record, an audit row), and one of those can land after the test that
// triggered it has returned. Postgres then reports a deadlock and kills the
// truncate, which fails an unrelated file and passes on the next run.
// Retrying is the honest answer: a deadlock means somebody else got there
// first, and by the next attempt the stray write has finished. Only 40P01 is
// retried, so a lock held for good still fails as it should.
for (let attempt = 0; ; attempt++) {
try {
await client.$executeRawUnsafe(
`TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE;`,
);
break;
} catch (error) {
const code = (error as { code?: string }).code;
const deadlock =
code === "40P01" || /deadlock detected/i.test(String(error));
if (!deadlock || attempt >= 4) throw error;
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
}
}
// Categories first — the tag rows FK into them.
if (categorySnapshot.length > 0) {