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:
parent
f949338ed7
commit
79201820a9
6 changed files with 35 additions and 52 deletions
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
29
server/db.ts
Normal 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>>];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue