Consolidate server postgres connections into one shared pool

Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).

Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
This commit is contained in:
Claude 2026-04-05 19:52:46 +00:00
parent f949338ed7
commit 79201820a9
No known key found for this signature in database
6 changed files with 35 additions and 52 deletions

View file

@ -220,11 +220,11 @@ describe("simulateChampionsStage", () => {
const teams = makeTeams(8); const teams = makeTeams(8);
// team-0 has highest Elo, team-7 has lowest // team-0 has highest Elo, team-7 has lowest
let wins = 0; let wins = 0;
for (let i = 0; i < 200; i++) { for (let i = 0; i < 1000; i++) {
const result = simulateChampionsStage(teams); const result = simulateChampionsStage(teams);
if (result.placements.get("team-0") === 1) wins++; if (result.placements.get("team-0") === 1) wins++;
} }
// Should win significantly more than 12.5% of the time (1/8 random) // Should win significantly more than 12.5% of the time (1/8 random)
expect(wins / 200).toBeGreaterThan(0.2); expect(wins / 1000).toBeGreaterThan(0.2);
}); });
}); });

View file

@ -1,27 +1,14 @@
import { createRequestHandler } from "@react-router/express"; import { createRequestHandler } from "@react-router/express";
import { drizzle } from "drizzle-orm/postgres-js";
import express from "express"; import express from "express";
import postgres from "postgres";
import type { ServerBuild } from "react-router"; import type { ServerBuild } from "react-router";
import { RouterContextProvider } from "react-router"; import { RouterContextProvider } from "react-router";
import { DatabaseContext } from "~/database/context"; import { DatabaseContext } from "~/database/context";
import * as schema from "~/database/schema"; import { db } from "./db";
import { expressValueContext } from "~/contexts/express"; import { expressValueContext } from "~/contexts/express";
export const app = express(); export const app = express();
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");
// In dev, ssrLoadModule re-evaluates this file on every request. Cache the
// client on globalThis so HMR re-loads reuse the same connection pool instead
// of leaking a new one each time.
declare global {
// TypeScript requires `var` in `declare global` blocks — `let`/`const` are not allowed here.
var __pgClient: ReturnType<typeof postgres> | undefined; // eslint-disable-line no-var
}
const client = (globalThis.__pgClient ??= postgres(process.env.DATABASE_URL));
const db = drizzle(client, { schema });
app.use((_, __, next) => DatabaseContext.run(db, next)); app.use((_, __, next) => DatabaseContext.run(db, next));
app.use( app.use(

29
server/db.ts Normal file
View file

@ -0,0 +1,29 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema";
// Cache the client on globalThis so HMR re-evaluations and multiple server
// modules share a single connection pool instead of each opening their own.
declare global {
// eslint-disable-next-line no-var
var __pgClient: ReturnType<typeof postgres> | undefined;
// eslint-disable-next-line no-var
var __pgDb: ReturnType<typeof drizzle<typeof schema>> | undefined;
}
function getDb() {
if (!globalThis.__pgDb) {
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required");
globalThis.__pgClient ??= postgres(process.env.DATABASE_URL);
globalThis.__pgDb = drizzle(globalThis.__pgClient, { schema });
}
return globalThis.__pgDb;
}
// Export a Proxy so callers can use `db.query.foo` etc. without calling getDb() themselves.
// The proxy defers the DATABASE_URL check until the first actual property access.
export const db = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, {
get(_target, prop) {
return getDb()[prop as keyof ReturnType<typeof drizzle<typeof schema>>];
},
});

View file

@ -1,17 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq, or } from "drizzle-orm"; import { eq, or } from "drizzle-orm";
import { createDailySnapshot } from "~/models/standings"; import { createDailySnapshot } from "~/models/standings";
import { logger } from "./logger"; import { logger } from "./logger";
import { db } from "./db";
// Create a dedicated database connection for the snapshot system
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL is required for snapshot system");
}
const client = postgres(connectionString);
const db = drizzle(client, { schema });
let snapshotInterval: NodeJS.Timeout | null = null; let snapshotInterval: NodeJS.Timeout | null = null;
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Check once per day (in milliseconds) const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Check once per day (in milliseconds)

View file

@ -1,23 +1,10 @@
import type { Socket } from "socket.io"; import type { Socket } from "socket.io";
import { Server as SocketIOServer } from "socket.io"; import { Server as SocketIOServer } from "socket.io";
import type { Server as HTTPServer } from "http"; import type { Server as HTTPServer } from "http";
import { drizzle } from "drizzle-orm/postgres-js";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq, and, asc } from "drizzle-orm"; import { eq, and, asc } from "drizzle-orm";
import { logger } from "./logger"; import { logger } from "./logger";
import { db } from "./db";
// Lazy-initialized DB for socket-level validation queries (team ownership checks)
let _socketDb: PostgresJsDatabase<typeof schema> | null = null;
function getSocketDb(): PostgresJsDatabase<typeof schema> {
if (!_socketDb) {
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");
_socketDb = drizzle(postgres(url), { schema });
}
return _socketDb;
}
// Socket event types // Socket event types
interface ServerToClientEvents { interface ServerToClientEvents {
@ -142,7 +129,6 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
// If teamId provided, validate it belongs to this season before tracking // If teamId provided, validate it belongs to this season before tracking
if (teamId) { if (teamId) {
try { try {
const db = getSocketDb();
const team = await db.query.teams.findFirst({ const team = await db.query.teams.findFirst({
where: and(eq(schema.teams.id, teamId), eq(schema.teams.seasonId, seasonId)), where: and(eq(schema.teams.id, teamId), eq(schema.teams.seasonId, seasonId)),
}); });
@ -185,7 +171,6 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
// tokens or flaky mobile networks. This socket-based sync provides an // tokens or flaky mobile networks. This socket-based sync provides an
// additional, more reliable path since the socket is already connected. // additional, more reliable path since the socket is already connected.
try { try {
const db = getSocketDb();
const [seasonData, picks, timerRows, queueItems] = await Promise.all([ const [seasonData, picks, timerRows, queueItems] = await Promise.all([
db.query.seasons.findFirst({ db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId), where: eq(schema.seasons.id, seasonId),

View file

@ -1,19 +1,10 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq, and, asc, sql } from "drizzle-orm"; import { eq, and, asc, sql } from "drizzle-orm";
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import { getSocketIO } from "./socket"; import { getSocketIO } from "./socket";
import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils"; import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils";
import { logger } from "./logger"; import { logger } from "./logger";
import { db } from "./db";
// Create a dedicated database connection for the timer
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL is required for timer system");
}
const client = postgres(connectionString);
const db = drizzle(client, { schema });
let timerInterval: NodeJS.Timeout | null = null; let timerInterval: NodeJS.Timeout | null = null;
let timerTickRunning = false; let timerTickRunning = false;