Skip to content

Commit 0e36f7c

Browse files
committed
feat(database): add infra-error classifier and read-retry util
Adds a shared classifier (isInfrastructureError / looksLikeConnectivityError) recognising connection-blip failures (P1001/P1002/P1008/P1017, ECONNRESET, "connection terminated", "server has closed the connection"), and withInfraRetry — a retry helper gated by an enabled kill-switch (default off) and the existing TokenBucketRetryBudget. Only for operations safe to run more than once (reads, or writes made idempotent); it never authorises retrying a bare non-idempotent write. TRI-13553
1 parent 43ecf15 commit 0e36f7c

5 files changed

Lines changed: 540 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "../generated/prisma";
22
export * from "./boundedIn";
3+
export * from "./infraError";
4+
export * from "./infraRetry";
35
export * from "./transaction";
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Prisma } from "../generated/prisma";
3+
import {
4+
isInfrastructureError,
5+
isRetryableInfrastructureError,
6+
looksLikeConnectivityError,
7+
} from "./infraError";
8+
9+
const known = (code: string, message = "") =>
10+
new Prisma.PrismaClientKnownRequestError(message, { code, clientVersion: "6.14.0" });
11+
12+
describe("isInfrastructureError", () => {
13+
it("treats connection-level Prisma codes as infrastructure errors", () => {
14+
for (const code of ["P1001", "P1002", "P1008", "P1017"]) {
15+
expect(isInfrastructureError(known(code, "boom"))).toBe(true);
16+
}
17+
});
18+
19+
it("does not treat query/validation errors as infrastructure errors", () => {
20+
expect(isInfrastructureError(known("P2025", "record not found"))).toBe(false);
21+
expect(isInfrastructureError(known("P2002", "unique constraint"))).toBe(false);
22+
});
23+
24+
it("treats P2010 as infrastructure only when the message looks like connectivity loss", () => {
25+
expect(isInfrastructureError(known("P2010", "Connection terminated unexpectedly"))).toBe(true);
26+
expect(isInfrastructureError(known("P2010", "syntax error at or near"))).toBe(false);
27+
});
28+
29+
it("treats init / panic / unknown request errors as infrastructure errors", () => {
30+
expect(
31+
isInfrastructureError(new Prisma.PrismaClientInitializationError("no db", "6.14.0"))
32+
).toBe(true);
33+
});
34+
35+
it("recognises raw connectivity errno / messages", () => {
36+
expect(isInfrastructureError({ code: "ECONNRESET" })).toBe(true);
37+
expect(isInfrastructureError(new Error("server has closed the connection"))).toBe(true);
38+
expect(isInfrastructureError(new Error("column does not exist"))).toBe(false);
39+
});
40+
});
41+
42+
describe("looksLikeConnectivityError", () => {
43+
it("matches known errno codes and message fragments", () => {
44+
expect(looksLikeConnectivityError({ code: "EHOSTUNREACH" })).toBe(true);
45+
expect(looksLikeConnectivityError(new Error("Can't reach database server"))).toBe(true);
46+
expect(looksLikeConnectivityError(new Error("relation does not exist"))).toBe(false);
47+
});
48+
});
49+
50+
describe("isRetryableInfrastructureError", () => {
51+
it("retries connection-level codes and connectivity errnos/messages", () => {
52+
for (const code of ["P1001", "P1002", "P1008", "P1017"]) {
53+
expect(isRetryableInfrastructureError(known(code, "boom"))).toBe(true);
54+
}
55+
expect(isRetryableInfrastructureError({ code: "ECONNRESET" })).toBe(true);
56+
expect(isRetryableInfrastructureError(new Error("server has closed the connection"))).toBe(
57+
true
58+
);
59+
});
60+
61+
it("does not retry query/validation errors", () => {
62+
expect(isRetryableInfrastructureError(known("P2025", "record not found"))).toBe(false);
63+
expect(isRetryableInfrastructureError(new Error("column does not exist"))).toBe(false);
64+
});
65+
66+
it("retries an init error only with a connectivity signal (not a permanent one)", () => {
67+
expect(
68+
isRetryableInfrastructureError(
69+
new Prisma.PrismaClientInitializationError("Can't reach database server", "6.14.0", "P1001")
70+
)
71+
).toBe(true);
72+
expect(
73+
isRetryableInfrastructureError(
74+
new Prisma.PrismaClientInitializationError(
75+
"Authentication failed against database server",
76+
"6.14.0",
77+
"P1000"
78+
)
79+
)
80+
).toBe(false);
81+
});
82+
83+
it("never retries a Rust-engine panic", () => {
84+
expect(
85+
isRetryableInfrastructureError(new Prisma.PrismaClientRustPanicError("panic", "6.14.0"))
86+
).toBe(false);
87+
});
88+
89+
it("retries an unknown-request error only with a connectivity signal", () => {
90+
expect(
91+
isRetryableInfrastructureError(
92+
new Prisma.PrismaClientUnknownRequestError("connection terminated unexpectedly", {
93+
clientVersion: "6.14.0",
94+
})
95+
)
96+
).toBe(true);
97+
expect(
98+
isRetryableInfrastructureError(
99+
new Prisma.PrismaClientUnknownRequestError("unexpected engine failure", {
100+
clientVersion: "6.14.0",
101+
})
102+
)
103+
).toBe(false);
104+
});
105+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Prisma } from "../generated/prisma";
2+
3+
// Prisma connectivity / infrastructure error codes — connection-level failures,
4+
// not query- or validation-level ones (e.g. P1001 "Can't reach database server").
5+
const INFRASTRUCTURE_PRISMA_CODES = new Set(["P1001", "P1002", "P1008", "P1017"]);
6+
7+
const CONNECTIVITY_ERRNO = new Set([
8+
"ECONNREFUSED",
9+
"ENOTFOUND",
10+
"ETIMEDOUT",
11+
"ECONNRESET",
12+
"EHOSTUNREACH",
13+
"EPIPE",
14+
]);
15+
16+
const CONNECTIVITY_MESSAGE =
17+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|database not reachable|can't reach database|connection terminated|server has closed the connection|timed out fetching a new connection/i;
18+
19+
/** True for an errno/message that looks like a lost or unreachable connection. */
20+
export function looksLikeConnectivityError(error: unknown): boolean {
21+
const e = error as { code?: unknown; message?: unknown };
22+
if (typeof e?.code === "string" && CONNECTIVITY_ERRNO.has(e.code)) {
23+
return true;
24+
}
25+
return typeof e?.message === "string" && CONNECTIVITY_MESSAGE.test(e.message);
26+
}
27+
28+
/**
29+
* True when `error` is a Prisma infrastructure/connectivity failure (DB
30+
* unreachable, timed out, connection dropped) rather than a query- or
31+
* validation-level error. Broad by design (matches the classifier used for
32+
* logging); for the retry decision use {@link isRetryableInfrastructureError}.
33+
*/
34+
export function isInfrastructureError(error: unknown): boolean {
35+
if (
36+
error instanceof Prisma.PrismaClientInitializationError ||
37+
error instanceof Prisma.PrismaClientRustPanicError ||
38+
error instanceof Prisma.PrismaClientUnknownRequestError
39+
) {
40+
return true;
41+
}
42+
43+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
44+
if (INFRASTRUCTURE_PRISMA_CODES.has(error.code)) {
45+
return true;
46+
}
47+
return error.code === "P2010" && looksLikeConnectivityError(error);
48+
}
49+
50+
return looksLikeConnectivityError(error);
51+
}
52+
53+
/**
54+
* True when `error` is a *transient* infrastructure failure worth retrying — a
55+
* genuine connectivity blip, not a permanent one. Narrower than
56+
* {@link isInfrastructureError}: an initialization or unknown-request error
57+
* counts only when it carries a connectivity signal (so a bad-URL / auth /
58+
* database-selection failure is NOT retried), and a Rust-engine panic is never
59+
* retried. This is the default retry gate for `withInfraRetry`.
60+
*/
61+
export function isRetryableInfrastructureError(error: unknown): boolean {
62+
if (error instanceof Prisma.PrismaClientRustPanicError) {
63+
return false;
64+
}
65+
66+
if (error instanceof Prisma.PrismaClientInitializationError) {
67+
return (
68+
(typeof error.errorCode === "string" && INFRASTRUCTURE_PRISMA_CODES.has(error.errorCode)) ||
69+
looksLikeConnectivityError(error)
70+
);
71+
}
72+
73+
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
74+
return looksLikeConnectivityError(error);
75+
}
76+
77+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
78+
if (INFRASTRUCTURE_PRISMA_CODES.has(error.code)) {
79+
return true;
80+
}
81+
return error.code === "P2010" && looksLikeConnectivityError(error);
82+
}
83+
84+
return looksLikeConnectivityError(error);
85+
}

0 commit comments

Comments
 (0)