-
-
Notifications
You must be signed in to change notification settings - Fork 317
fix: auto-retry database connection after server restart (#340) #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tighten the fallback string match to avoid retrying unrelated errors. The broad 🔧 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 |
||
|
|
||
| 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.