diff --git a/lib/db/retry.ts b/lib/db/retry.ts new file mode 100644 index 0000000..2daf749 --- /dev/null +++ b/lib/db/retry.ts @@ -0,0 +1,32 @@ +type RetryOptions = { + attempts?: number; + baseDelayMs?: number; +}; + +// The database proxy occasionally drops a request while it fails over. Most of +// those requests succeed if we try again a moment later, so retry with +// exponential backoff before giving up. +export async function withRetry( + operation: () => Promise, + { attempts = 3, baseDelayMs = 100 }: RetryOptions = {}, +): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error; + if (attempt === attempts) break; + + const delayMs = baseDelayMs * 2 ** (attempt - 1); + console.warn( + `[retry] attempt ${attempt} of ${attempts} failed, retrying in ${delayMs}ms`, + error, + ); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + + throw lastError; +} diff --git a/lib/payments/actions.ts b/lib/payments/actions.ts index bae7554..4c6497b 100644 --- a/lib/payments/actions.ts +++ b/lib/payments/actions.ts @@ -5,6 +5,7 @@ import { redirect } from "next/navigation"; import { z } from "zod"; import { db } from "@/lib/db/drizzle"; import { getUser } from "@/lib/db/queries"; +import { withRetry } from "@/lib/db/retry"; import { payments, teamMembers, teams } from "@/lib/db/schema"; const paymentSchema = z.object({ @@ -65,21 +66,25 @@ export async function processPayment(formData: FormData) { try { const validatedData = paymentSchema.parse(data); - await db.insert(payments).values({ - teamId: userTeam[0].teamId, - ...validatedData, - }); + // A dropped database request used to fail the whole checkout, so retry + // it a few times before showing the customer an error. + await withRetry(async () => { + await db.insert(payments).values({ + teamId: userTeam[0].teamId, + ...validatedData, + }); - await db - .update(teams) - .set({ - planName: validatedData.planName, - subscriptionStatus: "active", - updatedAt: new Date(), - }) - .where(eq(teams.id, userTeam[0].teamId)); + await db + .update(teams) + .set({ + planName: validatedData.planName, + subscriptionStatus: "active", + updatedAt: new Date(), + }) + .where(eq(teams.id, userTeam[0].teamId)); - redirect("/dashboard?payment=success"); + redirect("/dashboard?payment=success"); + }); } catch (error) { if (error instanceof z.ZodError) { throw new Error(error.issues.map((e) => e.message).join(", "));