Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
5 changes: 3 additions & 2 deletions app/api/health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import {
getNetworkPassphrase,
SorobanClientError,
} from "@/lib/soroban/client";
import { prisma } from "@/lib/prisma";
import { prisma, withQueryTimeout } from "@/lib/prisma";

export const runtime = "nodejs";

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" };
Expand Down
5 changes: 3 additions & 2 deletions app/api/v1/health/route.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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';
Expand Down
52 changes: 52 additions & 0 deletions docs/DB_POOLING_AND_TIMEOUTS.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 3 additions & 13 deletions lib/db.ts
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 22 additions & 0 deletions lib/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(promise: Promise<T>, ms = 5000): Promise<T> {
return new Promise<T>((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;