Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 117 additions & 3 deletions apps/web/src/server/db.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,129 @@
import { PrismaClient } from "@prisma/client";
import { Prisma, PrismaClient } from "@prisma/client";
import { env } from "~/env";
import { logger } from "./logger/log";

const MAX_RETRIES = 5;
const BASE_DELAY_MS = 100;
const MAX_DELAY_MS = 10000;

const RETRYABLE_ERROR_CODES = new Set([
"P1001", // Can't reach database server
"P1002", // Database server reached but timed out
"P1008", // Operations timed out
"P1017", // Server closed the connection
"P2024", // Timed out fetching a new connection from the pool
]);

// Only retry read-only operations to avoid re-running non-idempotent mutations
const READ_ONLY_OPERATIONS = new Set([
"findUnique",
"findUniqueOrThrow",
"findFirst",
"findFirstOrThrow",
"findMany",
"count",
"aggregate",
"groupBy",
]);

function isRetryableError(error: unknown): boolean {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return RETRYABLE_ERROR_CODES.has(error.code);
}

if (error instanceof Prisma.PrismaClientInitializationError) {
// Only retry transient connection errors, not permanent misconfigurations
// (e.g., invalid credentials, wrong database URL, schema errors)
return (
error.errorCode !== undefined && RETRYABLE_ERROR_CODES.has(error.errorCode)
);
}

if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes("econnrefused") ||
message.includes("etimedout") ||
message.includes("econnreset") ||
message.includes("connection") ||
message.includes("socket") ||
message.includes("timeout")
);
}

return false;
}
Comment on lines +29 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten the fallback string match to avoid retrying unrelated errors.

The broad "connection"/"socket" checks can match non-network errors (e.g., validation messages mentioning a connection field), causing unnecessary retries and noisy logs. Consider narrowing to explicit network signatures.

🔧 Suggested refinement
-    return (
-      message.includes("econnrefused") ||
-      message.includes("etimedout") ||
-      message.includes("econnreset") ||
-      message.includes("connection") ||
-      message.includes("socket") ||
-      message.includes("timeout")
-    );
+    return (
+      message.includes("econnrefused") ||
+      message.includes("etimedout") ||
+      message.includes("econnreset") ||
+      message.includes("socket hang up") ||
+      message.includes("timeout")
+    );
🤖 Prompt for AI Agents
In `@apps/web/src/server/db.ts` around lines 29 - 55, The fallback string matching
in isRetryableError is too broad (matching "connection"/"socket"/"timeout") and
can cause false positives; narrow it to explicit network error signatures
instead. Update the Error branch in isRetryableError to check for specific
tokens/phrases such as "ec CONNREFUSED"/"econnrefused"/"connection refused",
"etimedout"/"timed out", "econnreset"/"connection reset", "socket hang up",
"getaddrinfo ENOTFOUND"/"enotfound", "ehostunreach"/"host unreachable",
"econnaborted"/"connection aborted" (or an equivalent regex that matches these
exact network error patterns) instead of generic
"connection"/"socket"/"timeout"; keep the existing Prisma checks
(PrismaClientKnownRequestError, PrismaClientInitializationError and
RETRYABLE_ERROR_CODES) unchanged. Ensure the string comparisons are
case-insensitive and avoid matching generic words like "connection" used in
validation messages.


function calculateDelay(attempt: number): number {
const exponentialDelay = BASE_DELAY_MS * Math.pow(2, attempt);
const jitter = Math.random() * 100;
return Math.min(exponentialDelay + jitter, MAX_DELAY_MS);
}

async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

const createPrismaClient = () => {
logger.info("Creating Prisma client");
logger.info("Creating Prisma client with retry logic");
const client = new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});

return client;
return client.$extends({
query: {
async $allOperations({ operation, model, args, query }) {
// Skip retries for non-idempotent mutations (creates, updates, deletes)
if (!READ_ONLY_OPERATIONS.has(operation)) {
return await query(args);
}

let lastError: unknown;

for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await query(args);
} catch (error) {
lastError = error;

if (!isRetryableError(error)) {
throw error;
}

if (attempt < MAX_RETRIES - 1) {
const delay = calculateDelay(attempt);
logger.warn(
{
operation,
model,
attempt: attempt + 1,
maxRetries: MAX_RETRIES,
delayMs: delay,
error:
error instanceof Error ? error.message : "Unknown error",
},
`Database connection error, retrying...`
);
await sleep(delay);
}
}
}

logger.error(
{
operation,
model,
attempts: MAX_RETRIES,
error:
lastError instanceof Error ? lastError.message : "Unknown error",
},
`Database operation failed after ${MAX_RETRIES} retries`
);
throw lastError;
},
},
});
};

// eslint-disable-next-line no-undef
Expand Down