Skip to content
Open
Show file tree
Hide file tree
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
8 changes: 5 additions & 3 deletions cdk/lib/constructs/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ export class Database extends Construct implements ec2.IConnectable {

public getLambdaEnvironment(databaseName: string) {
const conn = this.getConnectionInfo();
// Aurora Serverless v2 cold start takes up to 15 seconds
// https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections/connection-pool
const option = '?pool_timeout=20&connect_timeout=20';
// connection_limit=1: Each Lambda instance handles one request at a time
// pool_timeout=30: Must be >= connect_timeout to allow Aurora Serverless v2 resume (~15s)
// connect_timeout=30: Aurora Serverless v2 auto-pause resume takes ~15s (longer after 24h+ pause)
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html
const option = '?connection_limit=1&pool_timeout=30&connect_timeout=30';
return {
DATABASE_HOST: conn.host,
DATABASE_NAME: databaseName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ exports[`Snapshot test 2`] = `
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -772,7 +772,7 @@ exports[`Snapshot test 2`] = `
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down Expand Up @@ -3406,7 +3406,7 @@ service iptables save",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -3455,7 +3455,7 @@ service iptables save",
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down Expand Up @@ -3783,7 +3783,7 @@ service iptables save",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -3832,7 +3832,7 @@ service iptables save",
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ exports[`Snapshot test 2`] = `
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -793,7 +793,7 @@ exports[`Snapshot test 2`] = `
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down Expand Up @@ -3236,7 +3236,7 @@ service iptables save",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -3285,7 +3285,7 @@ service iptables save",
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down Expand Up @@ -3589,7 +3589,7 @@ service iptables save",
],
},
"DATABASE_NAME": "main",
"DATABASE_OPTION": "?pool_timeout=20&connect_timeout=20",
"DATABASE_OPTION": "?connection_limit=1&pool_timeout=30&connect_timeout=30",
"DATABASE_PASSWORD": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -3638,7 +3638,7 @@ service iptables save",
"Endpoint.Port",
],
},
"/main?pool_timeout=20&connect_timeout=20",
"/main?connection_limit=1&pool_timeout=30&connect_timeout=30",
],
],
},
Expand Down
47 changes: 33 additions & 14 deletions webapp/src/jobs/migration-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,45 @@ export const handler: Handler = async (event, _) => {
// Currently we don't have any direct method to invoke prisma migration programmatically.
// As a workaround, we spawn migration script as a child process and wait for its completion.
// Please also refer to the following GitHub issue: https://github.com/prisma/prisma/issues/4703
try {
const exitCode = await new Promise((resolve, _) => {
await runPrismaDbPush(options);
};

// Aurora Serverless v2 may be resuming from auto-pause (0 ACU) during CDK deployment,
// which takes approximately 15 seconds. Retry transient connection errors with exponential backoff.
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html
async function runPrismaDbPush(options: string[], maxRetries = 5, baseDelay = 3000): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const { exitCode, stdout, stderr } = await new Promise<{
exitCode: number;
stdout: string;
stderr: string;
}>((resolve) => {
execFile(
path.resolve('./node_modules/prisma/build/index.js'),
['db', 'push', '--skip-generate'].concat(options),
(error, stdout, stderr) => {
console.log(stdout);
if (error != null) {
console.log(`prisma db push exited with error ${error.message}`);
resolve(error.code ?? 1);
} else {
resolve(0);
}
resolve({
exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
stdout,
stderr,
});
},
);
});

if (exitCode != 0) throw Error(`db push failed with exit code ${exitCode}`);
} catch (e) {
console.log(e);
throw e;
console.log(`prisma db push attempt ${attempt}/${maxRetries}`, { exitCode, stdout, stderr });

if (exitCode === 0) return;

const isRetryable =
stderr.includes('P1001') || stderr.includes("Can't reach database") || stderr.includes('Connection refused');

if (!isRetryable || attempt === maxRetries) {
throw new Error(`prisma db push failed after ${attempt} attempt(s): ${stderr}`);
}

const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
console.log(`Retrying prisma db push in ${Math.round(delay)}ms...`);
await new Promise((r) => setTimeout(r, delay));
}
};
}
64 changes: 61 additions & 3 deletions webapp/src/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,70 @@
import { PrismaClient } from '@prisma/client';
import { Prisma, PrismaClient } from '@prisma/client';

// https://www.prisma.io/docs/guides/nextjs

const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};

console.log(process.env.DATABASE_URL);
export const prisma = globalForPrisma.prisma || new PrismaClient({ log: ['query', 'info', 'warn', 'error'] });
// Determine if an error is a transient connection issue that may resolve on retry.
// Aurora Serverless v2 can drop connections due to idle_session_timeout (60s) or auto-pause,
// and resume takes approximately 15 seconds.
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html
function isRetryableError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const code = (error as { code?: string }).code;
if (
code === 'P2024' || // Connection pool timeout
code === 'P1001' || // Can't reach database server
code === 'P1017' // Server has closed the connection
) {
return true;
}
const msg = error.message;
return (
msg.includes('idle-session timeout') ||
msg.includes('terminating connection') ||
msg.includes('Connection terminated') ||
msg.includes('Timed out fetching a new connection from the connection pool') ||
msg.includes('ECONNRESET')
);
}

const basePrisma = new PrismaClient();

async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3, baseDelay = 500): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await fn();
if (attempt > 0) {
console.warn(`Prisma query succeeded after ${attempt} retry(s)`);
}
return result;
} catch (error) {
lastError = error;
if (attempt === maxRetries || !isRetryableError(error)) throw error;
// Discard stale connections before retrying
await basePrisma.$disconnect();
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 100;
console.warn(`Prisma retry attempt ${attempt + 1}/${maxRetries}, waiting ${Math.round(delay)}ms`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}

const retryExtension = Prisma.defineExtension({
name: 'retry-on-connection-error',
query: {
$allModels: {
async $allOperations({ args, query }) {
return withRetry(() => query(args));
},
},
},
});

export const prisma = basePrisma.$extends(retryExtension) as unknown as PrismaClient;

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Loading