Skip to content

Commit 688c2e7

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
fix(webapp): don't eagerly connect the webhook database clients
When hosted webhooks use a dedicated database (`WEBHOOK_DATABASE_URL`), webapp processes no longer open connections to it at startup. They connect on first use, so services that never handle webhooks hold no webhook database connections. Mono-RevId: 1650824151421dd3f296cadd8b1b12521021ae92
1 parent a482b8f commit 688c2e7

3 files changed

Lines changed: 109 additions & 17 deletions

File tree

‎apps/webapp/app/db.server.ts‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export const webhookPrisma: WebhookDatabase = singleton("webhookPrisma", () => {
252252
url: env.WEBHOOK_DATABASE_URL,
253253
clientType: "webhook-writer",
254254
connectionLimit: env.WEBHOOK_DATABASE_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT,
255+
eagerConnect: false,
255256
})
256257
)
257258
);
@@ -272,6 +273,7 @@ export const webhookPartitionPrisma: WebhookDatabase = singleton("webhookPartiti
272273
url: env.WEBHOOK_DATABASE_DIRECT_URL,
273274
clientType: "webhook-partitions",
274275
connectionLimit: 1,
276+
eagerConnect: false,
275277
})
276278
)
277279
);
@@ -291,6 +293,7 @@ export const webhookReplica: WebhookReplicaDatabase = singleton("webhookReplica"
291293
url: env.WEBHOOK_DATABASE_READ_REPLICA_URL,
292294
clientType: "webhook-reader",
293295
connectionLimit: env.WEBHOOK_DATABASE_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT,
296+
eagerConnect: false,
294297
})
295298
)
296299
)
@@ -744,13 +747,16 @@ export function buildWriterClient({
744747
poolTimeout,
745748
connectTimeout,
746749
useDriverAdapter = false,
750+
eagerConnect = true,
747751
}: {
748752
url: string;
749753
clientType: string;
750754
connectionLimit?: number;
751755
poolTimeout?: number;
752756
connectTimeout?: number;
753757
useDriverAdapter?: boolean;
758+
/** Open a connection at build time. Off for clients most processes never query. */
759+
eagerConnect?: boolean;
754760
}): PrismaClient {
755761
const databaseUrl = buildPrismaConnectionUrl(url, {
756762
connectionLimit: connectionLimit.toString(),
@@ -836,17 +842,19 @@ export function buildWriterClient({
836842
log: logConfig,
837843
});
838844

839-
registerDatabaseMetricsSource(
840-
driverPool
845+
let used = eagerConnect;
846+
registerDatabaseMetricsSource({
847+
...(driverPool
841848
? {
842849
clientType,
843850
usesDriverAdapter: true,
844851
client,
845852
pool: driverPool.pool,
846853
poolCounters: driverPool.poolCounters,
847854
}
848-
: { clientType, usesDriverAdapter: false, client }
849-
);
855+
: { clientType, usesDriverAdapter: false, client }),
856+
isActive: () => used,
857+
});
850858

851859
// Only use structured logging if we're not already logging to stdout
852860
if (process.env.PRISMA_LOG_TO_STDOUT !== "1") {
@@ -890,6 +898,18 @@ export function buildWriterClient({
890898
queryPerformanceMonitor.onQuery("writer", log);
891899
});
892900

901+
if (!eagerConnect) {
902+
return client.$extends({
903+
name: "first-query-marker",
904+
query: {
905+
$allOperations: ({ query, args }) => {
906+
used = true;
907+
return query(args);
908+
},
909+
},
910+
}) as unknown as PrismaClient;
911+
}
912+
893913
// Connect eagerly; Prisma will connect on use anyway.
894914
// Swallow the error when testing (DB likely unavailable)
895915
const connectPromise = client.$connect();
@@ -933,13 +953,16 @@ export function buildReplicaClient({
933953
poolTimeout,
934954
connectTimeout,
935955
useDriverAdapter = false,
956+
eagerConnect = true,
936957
}: {
937958
url: string;
938959
clientType: string;
939960
connectionLimit?: number;
940961
poolTimeout?: number;
941962
connectTimeout?: number;
942963
useDriverAdapter?: boolean;
964+
/** Open a connection at build time. Off for clients most processes never query. */
965+
eagerConnect?: boolean;
943966
}): PrismaClient {
944967
const replicaUrl = buildPrismaConnectionUrl(url, {
945968
connectionLimit: connectionLimit.toString(),
@@ -1025,17 +1048,19 @@ export function buildReplicaClient({
10251048
log: logConfig,
10261049
});
10271050

1028-
registerDatabaseMetricsSource(
1029-
driverPool
1051+
let used = eagerConnect;
1052+
registerDatabaseMetricsSource({
1053+
...(driverPool
10301054
? {
10311055
clientType,
10321056
usesDriverAdapter: true,
10331057
client: replicaClient,
10341058
pool: driverPool.pool,
10351059
poolCounters: driverPool.poolCounters,
10361060
}
1037-
: { clientType, usesDriverAdapter: false, client: replicaClient }
1038-
);
1061+
: { clientType, usesDriverAdapter: false, client: replicaClient }),
1062+
isActive: () => used,
1063+
});
10391064

10401065
// Only use structured logging if we're not already logging to stdout
10411066
if (process.env.PRISMA_LOG_TO_STDOUT !== "1") {
@@ -1078,6 +1103,18 @@ export function buildReplicaClient({
10781103
queryPerformanceMonitor.onQuery("replica", log);
10791104
});
10801105

1106+
if (!eagerConnect) {
1107+
return replicaClient.$extends({
1108+
name: "first-query-marker",
1109+
query: {
1110+
$allOperations: ({ query, args }) => {
1111+
used = true;
1112+
return query(args);
1113+
},
1114+
},
1115+
}) as unknown as PrismaClient;
1116+
}
1117+
10811118
// Connect eagerly; Prisma will connect on use anyway.
10821119
// Swallow the error when testing (DB likely unavailable)
10831120
const connectPromise = replicaClient.$connect();

‎apps/webapp/app/utils/databaseMetrics.server.ts‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export type DatabaseMetricsSource = {
2828
client: MetricsCapableClient;
2929
pool?: PoolLike;
3030
poolCounters?: { opened: () => number; closed: () => number };
31+
/** False until a lazy client's first query; reading engine metrics would connect it. */
32+
isActive?: () => boolean;
3133
};
3234

3335
type NormalizedPoolMetrics = {
@@ -131,14 +133,16 @@ export function normalizeDatabaseMetrics(
131133

132134
export async function collectDatabaseClientMetrics(): Promise<NormalizedDatabaseMetrics[]> {
133135
return Promise.all(
134-
Array.from(sources.values()).map(async (source) => {
135-
let json: PrismaMetricsJson | undefined;
136-
try {
137-
json = await source.client.$metrics.json();
138-
} catch {
139-
json = undefined;
140-
}
141-
return normalizeDatabaseMetrics(source, json);
142-
})
136+
Array.from(sources.values())
137+
.filter((source) => source.isActive?.() ?? true)
138+
.map(async (source) => {
139+
let json: PrismaMetricsJson | undefined;
140+
try {
141+
json = await source.client.$metrics.json();
142+
} catch {
143+
json = undefined;
144+
}
145+
return normalizeDatabaseMetrics(source, json);
146+
})
143147
);
144148
}

‎apps/webapp/test/runOpsDbTopology.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
sameDatabaseTarget,
77
selectRunOpsTopology,
88
} from "~/db.server";
9+
import { collectDatabaseClientMetrics } from "~/utils/databaseMetrics.server";
910

1011
const cp = { writer: {} as any, replica: {} as any };
1112

@@ -383,3 +384,53 @@ describe("selectRunOpsTopology (integration, real containers)", () => {
383384
}
384385
}, 120_000);
385386
});
387+
388+
describe("eagerConnect", () => {
389+
it("a client built with eagerConnect: false opens no connection until its first query, even when metrics are collected", async () => {
390+
const pg = await new PostgreSqlContainer("docker.io/postgres:14").start();
391+
const url = pg.getConnectionUri();
392+
const observer = buildWriterClient({ url, clientType: "observer", connectionLimit: 1 });
393+
const clients = [observer];
394+
const backends = async () => {
395+
const [row] = await observer.$queryRawUnsafe<{ n: number }[]>(
396+
`SELECT count(*)::int AS n FROM pg_stat_activity WHERE backend_type = 'client backend' AND pid <> pg_backend_pid()`
397+
);
398+
return row.n;
399+
};
400+
try {
401+
const baseline = await backends();
402+
403+
const lazyWriter = buildWriterClient({
404+
url,
405+
clientType: "lazy-writer",
406+
connectionLimit: 1,
407+
eagerConnect: false,
408+
});
409+
const lazyReplica = buildReplicaClient({
410+
url,
411+
clientType: "lazy-replica",
412+
connectionLimit: 1,
413+
eagerConnect: false,
414+
});
415+
clients.push(lazyWriter, lazyReplica);
416+
const collectedTypes = async () =>
417+
(await collectDatabaseClientMetrics()).map((metrics) => metrics.clientType);
418+
expect(await collectedTypes()).not.toContain("lazy-writer");
419+
await new Promise((resolve) => setTimeout(resolve, 500));
420+
expect(await backends()).toBe(baseline);
421+
422+
await lazyWriter.$queryRawUnsafe("SELECT 1");
423+
await lazyReplica.$queryRawUnsafe("SELECT 1");
424+
expect(await backends()).toBe(baseline + 2);
425+
expect(await collectedTypes()).toEqual(
426+
expect.arrayContaining(["lazy-writer", "lazy-replica"])
427+
);
428+
429+
clients.push(buildWriterClient({ url, clientType: "eager-writer", connectionLimit: 1 }));
430+
await expect.poll(backends, { timeout: 5_000 }).toBe(baseline + 3);
431+
} finally {
432+
await Promise.all(clients.map((client) => client.$disconnect()));
433+
await pg.stop();
434+
}
435+
}, 60_000);
436+
});

0 commit comments

Comments
 (0)