fix(webapp): honor the configured database connect timeout (#4513)
## Summary Every Prisma client built its connection URL with a `connection_timeout` query param, but the Postgres connector's parameter is `connect_timeout`. The misspelled param is silently ignored, so all clients fell back to Prisma's 5s default instead of the configured timeout. When establishing a new connection briefly took longer than 5s (for example during connection spikes), it failed with `Can't reach database server` even though the database was healthy. ## Fix All four client builders now construct their connection URL through one shared helper (`buildPrismaConnectionUrl`) that sets `connect_timeout`, so the configured value actually applies, and the parameter name lives in exactly one place. Covered by a unit test.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: fix
|
||||
---
|
||||
|
||||
Honor the configured database connection timeout so brief connection spikes no longer surface as spurious "can't reach database server" errors.
|
||||
@@ -14,6 +14,7 @@ import { z } from "zod";
|
||||
import { env } from "./env.server";
|
||||
import { logger } from "./services/logger.server";
|
||||
import { isValidDatabaseUrl } from "./utils/db";
|
||||
import { buildPrismaConnectionUrl } from "./utils/prismaConnectionUrl";
|
||||
import {
|
||||
captureInfrastructureErrors,
|
||||
infraErrorAlreadyLogged,
|
||||
@@ -395,11 +396,11 @@ export function buildWriterClient({
|
||||
url: string;
|
||||
clientType: string;
|
||||
}): PrismaClient {
|
||||
const databaseUrl = extendQueryParams(url, {
|
||||
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
application_name: env.SERVICE_NAME,
|
||||
const databaseUrl = buildPrismaConnectionUrl(url, {
|
||||
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
applicationName: env.SERVICE_NAME,
|
||||
});
|
||||
|
||||
console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`);
|
||||
@@ -542,11 +543,11 @@ export function buildReplicaClient({
|
||||
url: string;
|
||||
clientType: string;
|
||||
}): PrismaClient {
|
||||
const replicaUrl = extendQueryParams(url, {
|
||||
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
application_name: env.SERVICE_NAME,
|
||||
const replicaUrl = buildPrismaConnectionUrl(url, {
|
||||
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
applicationName: env.SERVICE_NAME,
|
||||
});
|
||||
|
||||
console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`);
|
||||
@@ -672,11 +673,11 @@ function buildRunOpsWriterClient({
|
||||
url: string;
|
||||
clientType: string;
|
||||
}): RunOpsPrismaClient {
|
||||
const databaseUrl = extendQueryParams(url, {
|
||||
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
application_name: env.SERVICE_NAME,
|
||||
const databaseUrl = buildPrismaConnectionUrl(url, {
|
||||
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
applicationName: env.SERVICE_NAME,
|
||||
});
|
||||
|
||||
console.log(`🔌 setting up run-ops prisma client to ${redactUrlSecrets(databaseUrl)}`);
|
||||
@@ -723,14 +724,13 @@ function buildRunOpsReplicaClient({
|
||||
url: string;
|
||||
clientType: string;
|
||||
}): RunOpsPrismaClient {
|
||||
const replicaUrl = extendQueryParams(url, {
|
||||
// The new run-ops replica connects unpooled, so allow capping it independently of the writer.
|
||||
connection_limit: (
|
||||
const replicaUrl = buildPrismaConnectionUrl(url, {
|
||||
connectionLimit: (
|
||||
env.RUN_OPS_DATABASE_READ_REPLICA_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT
|
||||
).toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
application_name: env.SERVICE_NAME,
|
||||
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
|
||||
applicationName: env.SERVICE_NAME,
|
||||
});
|
||||
|
||||
console.log(`🔌 setting up run-ops read replica connection to ${redactUrlSecrets(replicaUrl)}`);
|
||||
@@ -789,19 +789,6 @@ export function sameDatabaseTarget(a: string | undefined, b: string | undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) {
|
||||
const url = new URL(hrefOrUrl);
|
||||
const query = url.searchParams;
|
||||
|
||||
for (const [key, val] of Object.entries(queryParams)) {
|
||||
query.set(key, val);
|
||||
}
|
||||
|
||||
url.search = query.toString();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function redactUrlSecrets(hrefOrUrl: string | URL) {
|
||||
const url = new URL(hrefOrUrl);
|
||||
url.password = "";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildPrismaConnectionUrl } from "./prismaConnectionUrl";
|
||||
|
||||
describe("buildPrismaConnectionUrl", () => {
|
||||
it("sets connect_timeout (the Postgres connector parameter), not the ignored connection_timeout", () => {
|
||||
const url = buildPrismaConnectionUrl("postgresql://u:p@host:5432/db?schema=public", {
|
||||
connectionLimit: "10",
|
||||
poolTimeout: "0",
|
||||
connectTimeout: "20",
|
||||
applicationName: "svc",
|
||||
});
|
||||
|
||||
expect(url.searchParams.get("connect_timeout")).toBe("20");
|
||||
expect(url.searchParams.has("connection_timeout")).toBe(false);
|
||||
expect(url.searchParams.get("connection_limit")).toBe("10");
|
||||
expect(url.searchParams.get("pool_timeout")).toBe("0");
|
||||
expect(url.searchParams.get("application_name")).toBe("svc");
|
||||
});
|
||||
|
||||
it("preserves existing base query params", () => {
|
||||
const url = buildPrismaConnectionUrl(
|
||||
"postgresql://u:p@host:5432/db?schema=public&sslmode=require",
|
||||
{ connectionLimit: "5", poolTimeout: "10", connectTimeout: "20", applicationName: "svc" }
|
||||
);
|
||||
|
||||
expect(url.searchParams.get("schema")).toBe("public");
|
||||
expect(url.searchParams.get("sslmode")).toBe("require");
|
||||
expect(url.searchParams.get("connect_timeout")).toBe("20");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
export type PrismaConnectionParams = {
|
||||
connectionLimit: string;
|
||||
poolTimeout: string;
|
||||
connectTimeout: string;
|
||||
applicationName: string;
|
||||
};
|
||||
|
||||
export function buildPrismaConnectionUrl(
|
||||
baseUrl: string | URL,
|
||||
params: PrismaConnectionParams
|
||||
): URL {
|
||||
const url = new URL(baseUrl);
|
||||
url.searchParams.set("connection_limit", params.connectionLimit);
|
||||
url.searchParams.set("pool_timeout", params.poolTimeout);
|
||||
url.searchParams.set("connect_timeout", params.connectTimeout);
|
||||
url.searchParams.set("application_name", params.applicationName);
|
||||
return url;
|
||||
}
|
||||
Reference in New Issue
Block a user