diff --git a/.env.example b/.env.example index 1140858..5185c72 100644 --- a/.env.example +++ b/.env.example @@ -45,3 +45,21 @@ USDC_ISSUER_ADDRESS=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 NEXT_PUBLIC_APP_URL=http://localhost:3000 API_MAX_BODY_SIZE=1048576 +# ============================================ +# Database (Prisma) — pooling and timeouts +# For local dev with SQLite: +# DATABASE_URL="file:./dev.db" +# For Postgres (Node server): +# - Set moderate pool size: connection_limit=10 +# - Fail fast on connect: connect_timeout=5 (seconds) +# - Pool acquire timeout: pool_timeout=5000 (ms) +# Example: +# postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=5000&connect_timeout=5 +# For Serverless (e.g., Vercel + Neon/Supabase): +# - Keep pool very small: connection_limit=1-3 +# - Use pooled/serverless connection URL where supported +# Example: +# postgresql://user:pass@host:5432/db?connection_limit=3&pool_timeout=5000&connect_timeout=5 +DATABASE_URL= +# Optional app-level query timeout (ms) used by withQueryTimeout helper +PRISMA_QUERY_TIMEOUT_MS=5000 diff --git a/README.md b/README.md index 5988d99..82be7a0 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,18 @@ The application interacts with the following Soroban smart contracts on Stellar: - Verify contract addresses match network (testnet vs mainnet) - Contract ABIs are included in `lib/contracts/` directory +### Database Pooling and Timeouts + +- Prisma client is a singleton (lib/prisma.ts) to avoid multiple pools. +- Configure pooling and timeouts in DATABASE_URL for non-SQLite databases: + - connection_limit: max pool size (serverless: 1–3; node server: 10) + - pool_timeout: milliseconds to wait for a connection (e.g., 5000) + - connect_timeout: seconds to establish a TCP connection (e.g., 5) +- Application-level query timeout: withQueryTimeout helper (default 5s) used in health checks. +- For Vercel/serverless: use serverless-friendly DB and small connection_limit. + +Details: see docs/DB_POOLING_AND_TIMEOUTS.md + ### Health and Monitoring **Health Check Endpoint**: `GET /api/health` diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 7578a35..fc76dc0 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -13,7 +13,7 @@ import { getNetworkPassphrase, SorobanClientError, } from "@/lib/soroban/client"; -import { prisma } from "@/lib/prisma"; +import { prisma, withQueryTimeout } from "@/lib/prisma"; export const runtime = "nodejs"; @@ -21,7 +21,8 @@ export async function GET() { // ── 1. Database ───────────────────────────────────────────────── let database: { reachable: boolean; error?: string }; try { - await prisma.$queryRaw`SELECT 1`; + // Fast-fail query with 5s timeout to verify DB connectivity + await withQueryTimeout(prisma.$queryRaw`SELECT 1`, 5000); database = { reachable: true }; } catch (err: any) { database = { reachable: false, error: err?.message ?? "unreachable" }; diff --git a/app/api/v1/health/route.ts b/app/api/v1/health/route.ts index 89dbcb1..596977d 100644 --- a/app/api/v1/health/route.ts +++ b/app/api/v1/health/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import * as StellarSdk from '@stellar/stellar-sdk'; import prisma from '@/lib/db'; +import { withQueryTimeout } from '@/lib/prisma'; /** * Health check endpoint for monitoring system status and connectivity. @@ -18,8 +19,8 @@ export async function GET() { // 1. Database Check try { - // Run a simple query to ensure connectivity - await prisma.$queryRaw`SELECT 1`; + // Run a simple query with 5s timeout to ensure connectivity + await withQueryTimeout(prisma.$queryRaw`SELECT 1`, 5000); } catch (error) { console.error('[Health Check] Database connectivity error:', error); results.database = 'error'; diff --git a/docs/DB_POOLING_AND_TIMEOUTS.md b/docs/DB_POOLING_AND_TIMEOUTS.md new file mode 100644 index 0000000..edf8c2f --- /dev/null +++ b/docs/DB_POOLING_AND_TIMEOUTS.md @@ -0,0 +1,52 @@ +Database Pooling and Timeouts + +Overview +- This project uses Prisma as the database client. To avoid exhausting connections and long request hangs, configure connection pooling and timeouts via environment variables and minimal client-side helpers. + +Pool Size Guidance +- Local dev (SQLite): pooling not applicable; defaults are fine. +- Node server (single instance, Postgres): set a moderate pool. + - Example: min 1, max 10 connections. + - In connection string (Postgres): connection_limit=10 +- Serverless (Vercel + Neon/Supabase/Planetscale): keep pool very small. + - Example: min 1, max 3 connections. + - In connection string: connection_limit=1-3 + +Timeouts +- Connection timeout: fail fast when DB host is unreachable. + - Postgres example: connect_timeout=5 (seconds) +- Pool acquire timeout: fail if pool cannot provide a connection in time. + - Example: pool_timeout=5000 (milliseconds) +- Query timeout: application-level timeout to prevent long-hanging queries. + - Implemented via withQueryTimeout helper in lib/prisma.ts (default 5000ms). + +Configuration +- Set DATABASE_URL and pool/timeout params in your environment. + - Postgres example (Node server): + postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=5000&connect_timeout=5 + - Postgres example (Serverless): + postgresql://user:pass@host:5432/db?connection_limit=3&pool_timeout=5000&connect_timeout=5 +- Optional app-level query timeout: + - PRISMA_QUERY_TIMEOUT_MS=5000 + +Implementation Notes +- Prisma client singleton is defined in lib/prisma.ts. All modules import from there to avoid multiple pools. +- Health check endpoints run a fast query (SELECT 1) wrapped in a 5s timeout to reflect DB availability quickly. +- For Next.js serverless on Vercel: + - Prefer a serverless-friendly DB (Neon serverless or pooled connection string). + - Keep connection_limit low (1-3). + - Consider enabling function warm-up or scheduled pings to keep Prisma engine warm. + +Health Check +- /api/health and /api/v1/health perform: + - Database probe: SELECT 1 with 5s timeout via withQueryTimeout. + - If DB is unreachable or times out, the endpoint returns 503 and logs the error. + +Where to Edit +- lib/prisma.ts: Prisma client initialization and withQueryTimeout helper. +- app/api/health/route.ts and app/api/v1/health/route.ts: fast-fail DB query. + +Troubleshooting +- ECONNREFUSED or timeout: verify host, port, and connect_timeout. +- Pool timeout errors: increase connection_limit slightly or reduce concurrent load. +- Serverless spikes: keep function cold starts low; use pooled connection URLs. diff --git a/lib/db.ts b/lib/db.ts index 2895d10..d671981 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,14 +1,4 @@ -import { PrismaClient } from "@prisma/client"; - -declare global { - // eslint-disable-next-line no-var - var prisma: PrismaClient | undefined; -} - -const prisma = global.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== "production") { - global.prisma = prisma; -} - +// Deprecated duplicate Prisma client setup replaced with a single shared instance. +// Re-export the singleton Prisma client from lib/prisma to avoid multiple pools. +import { prisma } from "./prisma"; export default prisma; diff --git a/lib/prisma.ts b/lib/prisma.ts index 7299b02..3c3e1ae 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -3,10 +3,32 @@ import { PrismaClient } from '@prisma/client'; const globalForPrisma = global as unknown as { prisma: PrismaClient }; +// Pooling and timeouts configuration +// - For serverless (e.g., Vercel), keep pool small to avoid exhausting DB connections +// via connection string env vars (see README): +// - connection_limit (max pool size), pool_timeout (ms), connect_timeout (ms) +// - PrismaClient built-in config: +// - log: keep minimal to reduce overhead export const prisma = globalForPrisma.prisma || new PrismaClient({ log: ['error'], }); +// Helper to run queries with a timeout to avoid long hangs +export async function withQueryTimeout(promise: Promise, ms = 5000): Promise { + return new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms); + promise + .then((v) => { + clearTimeout(t); + resolve(v); + }) + .catch((e) => { + clearTimeout(t); + reject(e); + }); + }); +} + if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; \ No newline at end of file