diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 5b357d060..9366e32e1 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -1,7 +1,7 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; -import { Pool } from "pg"; import { readConfigFile } from "@server/lib/readConfigFile"; import { withReplicas } from "drizzle-orm/pg-core"; +import { createPool } from "./poolConfig"; function createDb() { const config = readConfigFile(); @@ -39,12 +39,17 @@ function createDb() { // Create connection pools instead of individual connections const poolConfig = config.postgres.pool; - const primaryPool = new Pool({ + const maxConnections = poolConfig?.max_connections || 20; + const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000; + const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000; + + const primaryPool = createPool( connectionString, - max: poolConfig?.max_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000 - }); + maxConnections, + idleTimeoutMs, + connectionTimeoutMs, + "primary" + ); const replicas = []; @@ -55,14 +60,16 @@ function createDb() { }) ); } else { + const maxReplicaConnections = + poolConfig?.max_replica_connections || 20; for (const conn of replicaConnections) { - const replicaPool = new Pool({ - connectionString: conn.connection_string, - max: poolConfig?.max_replica_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: - poolConfig?.connection_timeout_ms || 5000 - }); + const replicaPool = createPool( + conn.connection_string, + maxReplicaConnections, + idleTimeoutMs, + connectionTimeoutMs, + "replica" + ); replicas.push( DrizzlePostgres(replicaPool, { logger: process.env.QUERY_LOGGING == "true" @@ -84,4 +91,4 @@ export default db; export const primaryDb = db.$primary; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] ->[0]; +>[0]; \ No newline at end of file diff --git a/server/db/pg/logsDriver.ts b/server/db/pg/logsDriver.ts index 49e26f89f..146b8fb2f 100644 --- a/server/db/pg/logsDriver.ts +++ b/server/db/pg/logsDriver.ts @@ -1,9 +1,9 @@ import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; -import { Pool } from "pg"; import { readConfigFile } from "@server/lib/readConfigFile"; import { withReplicas } from "drizzle-orm/pg-core"; import { build } from "@server/build"; import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver"; +import { createPool } from "./poolConfig"; function createLogsDb() { // Only use separate logs database in SaaS builds @@ -42,12 +42,17 @@ function createLogsDb() { // Create separate connection pool for logs database const poolConfig = logsConfig?.pool || config.postgres?.pool; - const primaryPool = new Pool({ + const maxConnections = poolConfig?.max_connections || 20; + const idleTimeoutMs = poolConfig?.idle_timeout_ms || 30000; + const connectionTimeoutMs = poolConfig?.connection_timeout_ms || 5000; + + const primaryPool = createPool( connectionString, - max: poolConfig?.max_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000 - }); + maxConnections, + idleTimeoutMs, + connectionTimeoutMs, + "logs-primary" + ); const replicas = []; @@ -58,14 +63,16 @@ function createLogsDb() { }) ); } else { + const maxReplicaConnections = + poolConfig?.max_replica_connections || 20; for (const conn of replicaConnections) { - const replicaPool = new Pool({ - connectionString: conn.connection_string, - max: poolConfig?.max_replica_connections || 20, - idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, - connectionTimeoutMillis: - poolConfig?.connection_timeout_ms || 5000 - }); + const replicaPool = createPool( + conn.connection_string, + maxReplicaConnections, + idleTimeoutMs, + connectionTimeoutMs, + "logs-replica" + ); replicas.push( DrizzlePostgres(replicaPool, { logger: process.env.QUERY_LOGGING == "true" @@ -84,4 +91,4 @@ function createLogsDb() { export const logsDb = createLogsDb(); export default logsDb; -export const primaryLogsDb = logsDb.$primary; +export const primaryLogsDb = logsDb.$primary; \ No newline at end of file diff --git a/server/db/pg/poolConfig.ts b/server/db/pg/poolConfig.ts new file mode 100644 index 000000000..f753121c1 --- /dev/null +++ b/server/db/pg/poolConfig.ts @@ -0,0 +1,63 @@ +import { Pool, PoolConfig } from "pg"; +import logger from "@server/logger"; + +export function createPoolConfig( + connectionString: string, + maxConnections: number, + idleTimeoutMs: number, + connectionTimeoutMs: number +): PoolConfig { + return { + connectionString, + max: maxConnections, + idleTimeoutMillis: idleTimeoutMs, + connectionTimeoutMillis: connectionTimeoutMs, + // TCP keepalive to prevent silent connection drops by NAT gateways, + // load balancers, and other intermediate network devices (e.g. AWS + // NAT Gateway drops idle TCP connections after ~350s) + keepAlive: true, + keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle + // Allow connections to be released and recreated more aggressively + // to avoid stale connections building up + allowExitOnIdle: false + }; +} + +export function attachPoolErrorHandlers(pool: Pool, label: string): void { + pool.on("error", (err) => { + // This catches errors on idle clients in the pool. Without this + // handler an unexpected disconnect would crash the process. + logger.error( + `Unexpected error on idle ${label} database client: ${err.message}` + ); + }); + + pool.on("connect", (client) => { + // Set a statement timeout on every new connection so a single slow + // query can't block the pool forever + client.query("SET statement_timeout = '30s'").catch((err: Error) => { + logger.warn( + `Failed to set statement_timeout on ${label} client: ${err.message}` + ); + }); + }); +} + +export function createPool( + connectionString: string, + maxConnections: number, + idleTimeoutMs: number, + connectionTimeoutMs: number, + label: string +): Pool { + const pool = new Pool( + createPoolConfig( + connectionString, + maxConnections, + idleTimeoutMs, + connectionTimeoutMs + ) + ); + attachPoolErrorHandlers(pool, label); + return pool; +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index 4c783e9b2..fc2c6c382 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -275,6 +275,8 @@ export default function Page() { } } + const disabled = !isPaidUser(tierMatrix.orgOidc); + return ( <>
@@ -292,6 +294,9 @@ export default function Page() {
+ + +
@@ -812,9 +817,10 @@ export default function Page() { +
); }