diff --git a/app/api/admin/database-health/route.ts b/app/api/admin/database-health/route.ts new file mode 100644 index 0000000..7c62328 --- /dev/null +++ b/app/api/admin/database-health/route.ts @@ -0,0 +1,89 @@ +// Database connection health monitoring endpoint +import { type NextRequest, NextResponse } from "next/server"; +import { checkDatabaseConnection, prisma } from "@/lib/prisma"; + +export async function GET(request: NextRequest) { + try { + // Check if user has admin access (you may want to add proper auth here) + const authHeader = request.headers.get("authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Basic database connectivity check + const isConnected = await checkDatabaseConnection(); + + if (!isConnected) { + return NextResponse.json( + { + status: "unhealthy", + database: { + connected: false, + error: "Database connection failed", + }, + timestamp: new Date().toISOString(), + }, + { status: 503 } + ); + } + + // Get basic metrics + const metrics = await Promise.allSettled([ + // Count total sessions + prisma.session.count(), + // Count processing status records + prisma.sessionProcessingStatus.count(), + // Count recent AI requests + prisma.aIProcessingRequest.count({ + where: { + createdAt: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours + }, + }, + }), + ]); + + const [sessionsResult, statusResult, aiRequestsResult] = metrics; + + return NextResponse.json({ + status: "healthy", + database: { + connected: true, + connectionType: + process.env.USE_ENHANCED_POOLING === "true" + ? "enhanced_pooling" + : "standard", + }, + metrics: { + totalSessions: + sessionsResult.status === "fulfilled" + ? sessionsResult.value + : "error", + processingRecords: + statusResult.status === "fulfilled" ? statusResult.value : "error", + recentAIRequests: + aiRequestsResult.status === "fulfilled" + ? aiRequestsResult.value + : "error", + }, + environment: { + nodeEnv: process.env.NODE_ENV, + enhancedPooling: process.env.USE_ENHANCED_POOLING === "true", + connectionLimit: process.env.DATABASE_CONNECTION_LIMIT || "default", + poolTimeout: process.env.DATABASE_POOL_TIMEOUT || "default", + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("Database health check failed:", error); + + return NextResponse.json( + { + status: "error", + error: error instanceof Error ? error.message : "Unknown error", + timestamp: new Date().toISOString(), + }, + { status: 500 } + ); + } +} diff --git a/biome.json b/biome.json index 3d27efe..9289363 100644 --- a/biome.json +++ b/biome.json @@ -27,7 +27,11 @@ "noArrayIndexKey": "warn" }, "complexity": { - "noForEach": "off" + "noForEach": "off", + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 15 } + } } } }, diff --git a/docs/database-connection-pooling.md b/docs/database-connection-pooling.md new file mode 100644 index 0000000..378f691 --- /dev/null +++ b/docs/database-connection-pooling.md @@ -0,0 +1,181 @@ +# Database Connection Pooling Guide + +This document explains how to optimize database connection pooling for better performance and resource management in the LiveDash application. + +## Overview + +The application now supports two connection pooling modes: + +1. **Standard Pooling**: Default Prisma client connection pooling +2. **Enhanced Pooling**: Advanced PostgreSQL connection pooling with custom configuration + +## Configuration + +### Environment Variables + +Add these variables to your `.env.local` file: + +```bash +# Database Connection Pooling Configuration +DATABASE_CONNECTION_LIMIT=20 # Maximum connections in pool +DATABASE_POOL_TIMEOUT=10 # Idle timeout in seconds +USE_ENHANCED_POOLING=true # Enable advanced pooling (production recommended) + +# Optional: Add pool parameters to DATABASE_URL for additional control +DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10" +``` + +### Pooling Modes + +#### Standard Pooling (Default) + +- Uses Prisma's built-in connection pooling +- Simpler configuration +- Good for development and small-scale deployments + +#### Enhanced Pooling (Recommended for Production) + +- Uses PostgreSQL native connection pooling with `@prisma/adapter-pg` +- Advanced monitoring and health checks +- Better resource management +- Detailed connection metrics + +## Implementation Details + +### Fixed Issues + +1. **Multiple PrismaClient Instances**: + - ❌ Before: Each scheduler created its own PrismaClient + - ✅ After: All modules use singleton pattern from `lib/prisma.ts` + +2. **No Connection Management**: + - ❌ Before: No graceful shutdown or connection cleanup + - ✅ After: Proper cleanup on process termination + +3. **No Monitoring**: + - ❌ Before: No visibility into connection usage + - ✅ After: Health check endpoint and connection metrics + +### Key Files Modified + +- `lib/prisma.ts` - Enhanced singleton with pooling options +- `lib/database-pool.ts` - Advanced pooling configuration +- `lib/processingScheduler.ts` - Fixed to use singleton +- `lib/importProcessor.ts` - Fixed to use singleton +- `lib/processingStatusManager.ts` - Fixed to use singleton +- `lib/schedulers.ts` - Added graceful shutdown +- `app/api/admin/database-health/route.ts` - Monitoring endpoint + +## Monitoring + +### Health Check Endpoint + +Check database connection health: + +```bash +curl -H "Authorization: Bearer your-token" \ + http://localhost:3000/api/admin/database-health +``` + +Response includes: + +- Connection status +- Pool statistics (if enhanced pooling enabled) +- Basic metrics (session counts, etc.) +- Configuration details + +### Connection Metrics + +With enhanced pooling enabled, you'll see console logs for: + +- Connection acquisitions/releases +- Pool size changes +- Error events +- Health check results + +## Performance Benefits + +### Before Optimization + +- Multiple connection pools (one per scheduler) +- Potential connection exhaustion under load +- No connection monitoring +- Resource waste from idle connections + +### After Optimization + +- Single shared connection pool +- Configurable pool size and timeouts +- Connection health monitoring +- Graceful shutdown and cleanup +- Better resource utilization + +## Recommended Settings + +### Development + +```bash +DATABASE_CONNECTION_LIMIT=10 +DATABASE_POOL_TIMEOUT=30 +USE_ENHANCED_POOLING=false +``` + +### Production + +```bash +DATABASE_CONNECTION_LIMIT=20 +DATABASE_POOL_TIMEOUT=10 +USE_ENHANCED_POOLING=true +``` + +### High-Load Production + +```bash +DATABASE_CONNECTION_LIMIT=50 +DATABASE_POOL_TIMEOUT=5 +USE_ENHANCED_POOLING=true +``` + +## Troubleshooting + +### Connection Pool Exhaustion + +If you see "too many connections" errors: + +1. Increase `DATABASE_CONNECTION_LIMIT` +2. Check for connection leaks in application code +3. Monitor the health endpoint for pool statistics + +### Slow Database Queries + +If queries are timing out: + +1. Decrease `DATABASE_POOL_TIMEOUT` +2. Check database query performance +3. Consider connection pooling at the infrastructure level (PgBouncer) + +### Memory Usage + +If memory usage is high: + +1. Decrease `DATABASE_CONNECTION_LIMIT` +2. Enable enhanced pooling for better resource management +3. Monitor idle connection cleanup + +## Best Practices + +1. **Always use the singleton**: Import `prisma` from `lib/prisma.ts` +2. **Monitor connection usage**: Use the health endpoint regularly +3. **Set appropriate limits**: Don't over-provision connections +4. **Enable enhanced pooling in production**: Better resource management +5. **Implement graceful shutdown**: Ensure connections are properly closed +6. **Log connection events**: Monitor for issues and optimize accordingly + +## Next Steps + +Consider implementing: + +1. **Connection pooling middleware**: PgBouncer or similar +2. **Read replicas**: For read-heavy workloads +3. **Connection retry logic**: For handling temporary failures +4. **Metrics collection**: Prometheus/Grafana for detailed monitoring diff --git a/lib/database-pool.ts b/lib/database-pool.ts new file mode 100644 index 0000000..5f9322e --- /dev/null +++ b/lib/database-pool.ts @@ -0,0 +1,122 @@ +// Advanced database connection pooling configuration + +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "@prisma/client"; +import { Pool } from "pg"; +import { env } from "./env.js"; + +// Enhanced connection pool configuration +const createConnectionPool = () => { + // Parse DATABASE_URL to get connection parameters + const databaseUrl = new URL(env.DATABASE_URL); + + const pool = new Pool({ + host: databaseUrl.hostname, + port: Number.parseInt(databaseUrl.port) || 5432, + user: databaseUrl.username, + password: databaseUrl.password, + database: databaseUrl.pathname.slice(1), // Remove leading slash + ssl: databaseUrl.searchParams.get("sslmode") !== "disable", + + // Connection pool configuration + max: env.DATABASE_CONNECTION_LIMIT, // Maximum number of connections + min: 2, // Minimum number of connections to maintain + idleTimeoutMillis: env.DATABASE_POOL_TIMEOUT * 1000, // Close idle connections after timeout + connectionTimeoutMillis: 10000, // Connection timeout + maxUses: 1000, // Maximum uses per connection before cycling + allowExitOnIdle: true, // Allow process to exit when all connections are idle + + // Health check configuration + query_timeout: 30000, // Query timeout + keepAlive: true, + keepAliveInitialDelayMillis: 30000, + }); + + // Connection pool event handlers + pool.on("connect", (_client) => { + console.log( + `Database connection established. Active connections: ${pool.totalCount}` + ); + }); + + pool.on("acquire", (_client) => { + console.log( + `Connection acquired from pool. Waiting: ${pool.waitingCount}, Idle: ${pool.idleCount}` + ); + }); + + pool.on("release", (_client) => { + console.log( + `Connection released to pool. Active: ${pool.totalCount - pool.idleCount}, Idle: ${pool.idleCount}` + ); + }); + + pool.on("error", (err) => { + console.error("Database pool error:", err); + }); + + pool.on("remove", (_client) => { + console.log( + `Connection removed from pool. Total connections: ${pool.totalCount}` + ); + }); + + return pool; +}; + +// Create adapter with connection pool +export const createEnhancedPrismaClient = () => { + const pool = createConnectionPool(); + const adapter = new PrismaPg(pool); + + return new PrismaClient({ + adapter, + log: + process.env.NODE_ENV === "development" + ? ["query", "info", "warn", "error"] + : ["error"], + }); +}; + +// Connection pool monitoring utilities +export const getPoolStats = (pool: Pool) => { + return { + totalConnections: pool.totalCount, + idleConnections: pool.idleCount, + waitingQueries: pool.waitingCount, + activeConnections: pool.totalCount - pool.idleCount, + }; +}; + +// Health check for the connection pool +export const checkPoolHealth = async ( + pool: Pool +): Promise<{ + healthy: boolean; + stats: ReturnType; + error?: string; +}> => { + try { + const client = await pool.connect(); + await client.query("SELECT 1"); + client.release(); + + return { + healthy: true, + stats: getPoolStats(pool), + }; + } catch (error) { + return { + healthy: false, + stats: getPoolStats(pool), + error: error instanceof Error ? error.message : String(error), + }; + } +}; + +// Graceful pool shutdown +export const shutdownPool = async (pool: Pool) => { + console.log("Shutting down database connection pool..."); + await pool.end(); + console.log("Database connection pool shut down successfully."); +}; diff --git a/lib/env.ts b/lib/env.ts index f13f0c7..e9b3e8f 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -103,6 +103,16 @@ export const env = { 5 ), + // Database Connection Pooling + DATABASE_CONNECTION_LIMIT: parseIntWithDefault( + process.env.DATABASE_CONNECTION_LIMIT, + 20 + ), + DATABASE_POOL_TIMEOUT: parseIntWithDefault( + process.env.DATABASE_POOL_TIMEOUT, + 10 + ), + // Server PORT: parseIntWithDefault(process.env.PORT, 3000), } as const; diff --git a/lib/importProcessor.ts b/lib/importProcessor.ts index b6524ba..a85c64d 100644 --- a/lib/importProcessor.ts +++ b/lib/importProcessor.ts @@ -1,19 +1,14 @@ // SessionImport to Session processor -import { - PrismaClient, - ProcessingStage, - SentimentCategory, -} from "@prisma/client"; +import { ProcessingStage, SentimentCategory } from "@prisma/client"; import cron from "node-cron"; import { getSchedulerConfig } from "./env"; +import { prisma } from "./prisma.js"; import { ProcessingStatusManager } from "./processingStatusManager"; import { fetchTranscriptContent, isValidTranscriptUrl, } from "./transcriptFetcher"; -const prisma = new PrismaClient(); - interface ImportRecord { id: string; companyId: string; diff --git a/lib/prisma.ts b/lib/prisma.ts index 6ba62e6..25cbdbe 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,20 +1,67 @@ -// Simple Prisma client setup +// Enhanced Prisma client setup with connection pooling import { PrismaClient } from "@prisma/client"; +import { createEnhancedPrismaClient } from "./database-pool.js"; +import { env } from "./env.js"; // Add prisma to the NodeJS global type -// This approach avoids NodeJS.Global which is not available - -// Prevent multiple instances of Prisma Client in development declare const global: { prisma: PrismaClient | undefined; }; -// Initialize Prisma Client -const prisma = global.prisma || new PrismaClient(); +// Connection pooling configuration +const createPrismaClient = () => { + // Use enhanced pooling in production or when explicitly enabled + const useEnhancedPooling = + process.env.NODE_ENV === "production" || + process.env.USE_ENHANCED_POOLING === "true"; -// Save in global if we're in development + if (useEnhancedPooling) { + console.log("Using enhanced database connection pooling with PG adapter"); + return createEnhancedPrismaClient(); + } + + // Default Prisma client with basic configuration + return new PrismaClient({ + log: + process.env.NODE_ENV === "development" + ? ["query", "error", "warn"] + : ["error"], + datasources: { + db: { + url: env.DATABASE_URL, + }, + }, + }); +}; + +// Initialize Prisma Client with singleton pattern +const prisma = global.prisma || createPrismaClient(); + +// Save in global if we're in development to prevent multiple instances if (process.env.NODE_ENV !== "production") { global.prisma = prisma; } +// Graceful shutdown handling +const gracefulShutdown = async () => { + console.log("Gracefully disconnecting from database..."); + await prisma.$disconnect(); + process.exit(0); +}; + +// Handle process termination signals +process.on("SIGINT", gracefulShutdown); +process.on("SIGTERM", gracefulShutdown); + +// Connection health check +export const checkDatabaseConnection = async (): Promise => { + try { + await prisma.$queryRaw`SELECT 1`; + return true; + } catch (error) { + console.error("Database connection failed:", error); + return false; + } +}; + export { prisma }; diff --git a/lib/processingScheduler.ts b/lib/processingScheduler.ts index 19e4ee7..8a44111 100644 --- a/lib/processingScheduler.ts +++ b/lib/processingScheduler.ts @@ -1,17 +1,16 @@ // Enhanced session processing scheduler with AI cost tracking and question management import { - PrismaClient, ProcessingStage, type SentimentCategory, type SessionCategory, } from "@prisma/client"; import cron from "node-cron"; import fetch from "node-fetch"; +import { prisma } from "./prisma.js"; import { ProcessingStatusManager } from "./processingStatusManager"; import { getSchedulerConfig } from "./schedulerConfig"; -const prisma = new PrismaClient(); const OPENAI_API_KEY = process.env.OPENAI_API_KEY; const OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; const DEFAULT_MODEL = process.env.OPENAI_MODEL || "gpt-4o"; diff --git a/lib/processingStatusManager.ts b/lib/processingStatusManager.ts index b58332e..3b105c1 100644 --- a/lib/processingStatusManager.ts +++ b/lib/processingStatusManager.ts @@ -1,10 +1,5 @@ -import { - PrismaClient, - ProcessingStage, - ProcessingStatus, -} from "@prisma/client"; - -const prisma = new PrismaClient(); +import { ProcessingStage, ProcessingStatus } from "@prisma/client"; +import { prisma } from "./prisma.js"; // Type-safe metadata interfaces interface ProcessingMetadata { diff --git a/lib/schedulers.ts b/lib/schedulers.ts index 4fc4ef8..938396c 100644 --- a/lib/schedulers.ts +++ b/lib/schedulers.ts @@ -1,5 +1,6 @@ -// Combined scheduler initialization +// Combined scheduler initialization with graceful shutdown +import { prisma } from "./prisma.js"; import { startProcessingScheduler } from "./processingScheduler"; import { startCsvImportScheduler } from "./scheduler"; @@ -16,4 +17,44 @@ export function initializeSchedulers() { startProcessingScheduler(); console.log("All schedulers initialized successfully"); + + // Set up graceful shutdown for schedulers + setupGracefulShutdown(); +} + +/** + * Set up graceful shutdown handling for schedulers and database connections + */ +function setupGracefulShutdown() { + const shutdown = async (signal: string) => { + console.log(`\nReceived ${signal}. Starting graceful shutdown...`); + + try { + // Disconnect from database + await prisma.$disconnect(); + console.log("Database connections closed."); + + // Exit the process + process.exit(0); + } catch (error) { + console.error("Error during shutdown:", error); + process.exit(1); + } + }; + + // Handle various termination signals + process.on("SIGTERM", () => shutdown("SIGTERM")); + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGUSR2", () => shutdown("SIGUSR2")); // Nodemon restart + + // Handle uncaught exceptions + process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); + shutdown("uncaughtException"); + }); + + process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); + shutdown("unhandledRejection"); + }); }