mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
import { Prisma, PrismaClient } from "@/generated/prisma/client";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
|
|
const globalForPrisma = globalThis as unknown as {
|
|
prisma: PrismaClient | undefined;
|
|
};
|
|
|
|
const DEFAULT_CONNECTION_BUDGET = 20;
|
|
const DEFAULT_POOL_TIMEOUT_SECONDS = 20;
|
|
const PG_BOSS_CONNECTIONS = 2;
|
|
|
|
function positiveInteger(raw: string | undefined): number | null {
|
|
if (!raw) return null;
|
|
const parsed = Number.parseInt(raw, 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
}
|
|
|
|
function databaseUrlParameter(name: string): string | undefined {
|
|
const rawUrl = process.env.DATABASE_URL;
|
|
if (!rawUrl) return undefined;
|
|
try {
|
|
return new URL(rawUrl).searchParams.get(name) ?? undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Total PostgreSQL connections this process may own. Compose already exposes
|
|
* this as DB_CONNECTION_LIMIT through DATABASE_URL's `connection_limit`.
|
|
* DATABASE_POOL_MAX remains a backward-compatible explicit override.
|
|
*/
|
|
export function getConnectionBudget(): number {
|
|
const configured =
|
|
positiveInteger(process.env.DB_CONNECTION_LIMIT) ??
|
|
positiveInteger(process.env.DATABASE_POOL_MAX) ??
|
|
positiveInteger(databaseUrlParameter("connection_limit")) ??
|
|
DEFAULT_CONNECTION_BUDGET;
|
|
return Math.max(2, configured);
|
|
}
|
|
|
|
export function getPgBossPoolMax(): number {
|
|
return Math.min(PG_BOSS_CONNECTIONS, getConnectionBudget() - 1);
|
|
}
|
|
|
|
export function getPrismaPoolMax(): number {
|
|
return getConnectionBudget() - getPgBossPoolMax();
|
|
}
|
|
|
|
export function getPoolConnectionTimeoutMs(): number {
|
|
const seconds =
|
|
positiveInteger(process.env.DB_POOL_TIMEOUT) ??
|
|
positiveInteger(databaseUrlParameter("pool_timeout")) ??
|
|
DEFAULT_POOL_TIMEOUT_SECONDS;
|
|
return seconds * 1_000;
|
|
}
|
|
|
|
/**
|
|
* Per-session DB statement timeout in milliseconds.
|
|
*
|
|
* Without this a single pathological query or lock wait holds one of the
|
|
* (default 20) pool slots indefinitely; 20 such queries permanently exhaust
|
|
* the pool and every DB-backed route stops serving — the single highest-impact
|
|
* availability gap (A-1). `statement_timeout` caps how long the server runs one
|
|
* statement; `idle_in_transaction_session_timeout` reaps a connection wedged
|
|
* mid-transaction (a client that opened a tx and stalled). Both are applied at
|
|
* connection-establishment via the libpq `options` startup parameter, so every
|
|
* pooled session inherits them.
|
|
*
|
|
* Env-overridable via `DATABASE_STATEMENT_TIMEOUT_MS`. Set to `0` to disable
|
|
* (legacy unbounded behaviour). Default 60 s — bounds a genuinely runaway query
|
|
* (the A-1 goal: never hold a pool slot forever) while leaving ample headroom
|
|
* for the heaviest legitimate read, the live-aggregate analytics fallback on a
|
|
* coverage miss over a large account, which can run well past 15 s. Heavy admin
|
|
* one-shots (drain/backfill) can still dial their own timeout if needed.
|
|
*/
|
|
export function getStatementTimeoutMs(): number {
|
|
const raw = process.env.DATABASE_STATEMENT_TIMEOUT_MS;
|
|
if (raw !== undefined && raw !== "") {
|
|
const parsed = Number.parseInt(raw, 10);
|
|
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
}
|
|
return 60_000;
|
|
}
|
|
|
|
/**
|
|
* Build the libpq `options` startup string applying the session timeouts, or
|
|
* `undefined` when disabled (timeout 0). Passed straight through `PrismaPg` to
|
|
* the underlying `pg.Pool`, which forwards it as the connection's `options`
|
|
* startup parameter so every session is timeout-bounded from the first query.
|
|
*/
|
|
export function buildSessionOptions(): string | undefined {
|
|
const timeoutMs = getStatementTimeoutMs();
|
|
if (timeoutMs <= 0) return undefined;
|
|
return `-c statement_timeout=${timeoutMs} -c idle_in_transaction_session_timeout=${timeoutMs}`;
|
|
}
|
|
|
|
function createPrismaClient() {
|
|
const sessionOptions = buildSessionOptions();
|
|
const adapter = new PrismaPg({
|
|
connectionString: process.env.DATABASE_URL!,
|
|
max: getPrismaPoolMax(),
|
|
connectionTimeoutMillis: getPoolConnectionTimeoutMs(),
|
|
...(sessionOptions ? { options: sessionOptions } : {}),
|
|
});
|
|
return new PrismaClient({ adapter });
|
|
}
|
|
|
|
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
globalForPrisma.prisma = prisma;
|
|
}
|
|
|
|
// Prisma's `InputJsonValue` requires an explicit index signature that
|
|
// typed application shapes (Zod-validated, hand-written interfaces)
|
|
// don't carry. Every JSON-column write would otherwise repeat the same
|
|
// `value as unknown as Prisma.InputJsonValue` escape hatch — this one
|
|
// helper centralises the cast so the WHY stays in a single place.
|
|
export const toJson = <T>(v: T) => v as unknown as Prisma.InputJsonValue;
|