const { Pool } = require("pg"); const crypto = require("crypto"); const logger = require("./logger"); require("dotenv").config(); const pool = new Pool({ host: process.env.DB_HOST || "localhost", port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || "skyartshop", user: process.env.DB_USER || "skyartapp", password: process.env.DB_PASSWORD, max: 30, // Increased to 30 for higher concurrency min: 10, // Keep 10 connections warm for instant response idleTimeoutMillis: 60000, connectionTimeoutMillis: 3000, application_name: "skyartshop-api", keepAlive: true, // TCP keepalive keepAliveInitialDelayMillis: 10000, statement_timeout: 30000, // 30s query timeout query_timeout: 30000, // SAFEGUARD: Force query timeout at pool level }); // SAFEGUARD: Track pool health let poolConnected = false; let connectionAttempts = 0; const MAX_CONNECTION_ATTEMPTS = 3; pool.on("connect", (client) => { poolConnected = true; connectionAttempts = 0; logger.info("✓ PostgreSQL connected", { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); }); pool.on("error", (err, client) => { poolConnected = false; connectionAttempts++; logger.error("💥 PostgreSQL pool error", { error: err.message, code: err.code, attempts: connectionAttempts, pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }, }); // SAFEGUARD: Critical failure detection if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) { logger.error( "🚨 Database connection critically unstable - manual intervention required" ); } }); pool.on("acquire", (client) => { logger.debug("Pool client acquired", { total: pool.totalCount, idle: pool.idleCount, }); }); pool.on("release", (err, client) => { if (err) { logger.warn("Client released with error", { error: err.message }); } }); // Query cache for SELECT statements with crypto-based keys const queryCache = new Map(); const queryCacheOrder = []; // LRU tracking const QUERY_CACHE_TTL = 15000; // 15 seconds (increased) const QUERY_CACHE_MAX_SIZE = 500; // 500 cached queries (increased) const SLOW_QUERY_THRESHOLD = 50; // 50ms threshold (stricter) const QUERY_TIMEOUT = 35000; // SAFEGUARD: 35s query timeout (slightly higher than pool's 30s) // Generate fast cache key using crypto hash const getCacheKey = (text, params) => { const hash = crypto.createHash("md5"); hash.update(text); if (params) hash.update(JSON.stringify(params)); return hash.digest("hex"); }; const query = async (text, params) => { const start = Date.now(); const isSelect = text.trim().toUpperCase().startsWith("SELECT"); // Check cache for SELECT queries if (isSelect) { const cacheKey = getCacheKey(text, params); const cached = queryCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < QUERY_CACHE_TTL) { logger.debug("Query cache hit", { duration: Date.now() - start }); return cached.data; } } try { // SAFEGUARD: Add query timeout wrapper const queryPromise = pool.query(text, params); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject( new Error( `Query timeout after ${QUERY_TIMEOUT}ms: ${text.substring( 0, 50 )}...` ) ); }, QUERY_TIMEOUT); }); const res = await Promise.race([queryPromise, timeoutPromise]); const duration = Date.now() - start; // Cache SELECT queries with LRU eviction if (isSelect) { const cacheKey = getCacheKey(text, params); // LRU eviction if (queryCache.size >= QUERY_CACHE_MAX_SIZE) { const oldestKey = queryCacheOrder.shift(); if (oldestKey) queryCache.delete(oldestKey); } queryCache.set(cacheKey, { data: res, timestamp: Date.now() }); queryCacheOrder.push(cacheKey); } // Log slow queries if (duration > SLOW_QUERY_THRESHOLD) { logger.warn("Slow query", { duration, text: text.substring(0, 100), rows: res.rowCount, params: params?.length || 0, }); } return res; } catch (error) { const duration = Date.now() - start; logger.error("Query error", { error: error.message, code: error.code, duration, text: text.substring(0, 100), }); // SAFEGUARD: Clear potentially corrupted cache entry if (isSelect) { const cacheKey = getCacheKey(text, params); queryCache.delete(cacheKey); const index = queryCacheOrder.indexOf(cacheKey); if (index > -1) { queryCacheOrder.splice(index, 1); } } throw error; } }; // Transaction helper const transaction = async (callback) => { const client = await pool.connect(); try { await client.query("BEGIN"); const result = await callback(client); await client.query("COMMIT"); return result; } catch (error) { await client.query("ROLLBACK"); logger.error("Transaction rolled back:", error); throw error; } finally { client.release(); } }; // Batch query execution for parallel operations const batchQuery = async (queries) => { try { const results = await Promise.all( queries.map(({ text, params }) => query(text, params)) ); return results; } catch (error) { logger.error("Batch query error:", error); throw error; } }; // Clear query cache (useful for cache invalidation) const clearQueryCache = (pattern) => { if (pattern) { // Clear specific pattern for (const key of queryCache.keys()) { if (key.includes(pattern)) { queryCache.delete(key); } } } else { // Clear all queryCache.clear(); queryCacheOrder.length = 0; } logger.info("Query cache cleared", { pattern: pattern || "all" }); }; // Health check with pool metrics const healthCheck = async (timeoutMs = 5000) => { // SAFEGUARD: Wrap health check in timeout promise const healthPromise = (async () => { try { const result = await query( "SELECT NOW() as time, current_database() as database" ); return { healthy: true, database: result.rows[0].database, timestamp: result.rows[0].time, pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, connected: poolConnected, }, cache: { size: queryCache.size, maxSize: QUERY_CACHE_MAX_SIZE, }, }; } catch (error) { logger.error("Database health check failed:", error); return { healthy: false, error: error.message, pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, connected: poolConnected, }, }; } })(); // SAFEGUARD: Add timeout protection const timeoutPromise = new Promise((_, reject) => { setTimeout( () => reject(new Error(`Health check timeout after ${timeoutMs}ms`)), timeoutMs ); }); return Promise.race([healthPromise, timeoutPromise]); }; // SAFEGUARD: Graceful pool shutdown for scripts/testing const closePool = async () => { try { await pool.end(); logger.info("Database pool closed gracefully"); return true; } catch (error) { logger.error("Error closing database pool:", error); return false; } }; // SAFEGUARD: Get pool status for monitoring const getPoolStatus = () => ({ total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, connected: poolConnected, cacheSize: queryCache.size, }); module.exports = { pool, query, transaction, batchQuery, clearQueryCache, healthCheck, closePool, getPoolStatus, };