UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

331 lines (330 loc) 9.74 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { Pool } from "pg"; import { EventEmitter } from "events"; import { logger } from "../monitoring/logger.js"; import { DatabaseError, ErrorCode } from "../errors/index.js"; class ConnectionPool extends EventEmitter { pool; config; metrics; health; healthCheckTimer; metricsTimer; startTime; badConnections = /* @__PURE__ */ new Set(); acquireTimes = []; constructor(config) { super(); this.config = this.normalizeConfig(config); this.startTime = /* @__PURE__ */ new Date(); this.metrics = { totalConnections: 0, idleConnections: 0, activeConnections: 0, waitingRequests: 0, totalAcquired: 0, totalReleased: 0, totalErrors: 0, averageAcquireTime: 0, peakConnections: 0, uptime: 0 }; this.health = { isHealthy: false, lastCheck: /* @__PURE__ */ new Date(), consecutiveFailures: 0, totalChecks: 0, totalFailures: 0, averageResponseTime: 0 }; this.pool = new Pool(this.config); this.setupPoolEvents(); if (this.config.enableMetrics) { this.startMonitoring(); } } normalizeConfig(config) { return { ...config, min: config.min ?? 2, max: config.max ?? 10, idleTimeoutMillis: config.idleTimeoutMillis ?? 3e4, connectionTimeoutMillis: config.connectionTimeoutMillis ?? 5e3, healthCheckInterval: config.healthCheckInterval ?? 3e4, healthCheckQuery: config.healthCheckQuery ?? "SELECT 1", retryOnFailure: config.retryOnFailure ?? true, maxRetries: config.maxRetries ?? 3, retryDelayMs: config.retryDelayMs ?? 1e3, enableMetrics: config.enableMetrics ?? true, metricsInterval: config.metricsInterval ?? 6e4 }; } setupPoolEvents() { this.pool.on("connect", (client) => { logger.debug("New database connection established"); this.metrics.totalConnections++; this.updatePeakConnections(); this.emit("connect", client); }); this.pool.on("acquire", (client) => { this.metrics.totalAcquired++; this.emit("acquire", client); }); this.pool.on("release", (client) => { this.metrics.totalReleased++; this.emit("release", client); }); this.pool.on("remove", (client) => { logger.debug("Database connection removed from pool"); this.metrics.totalConnections--; this.emit("remove", client); }); this.pool.on("error", (error) => { logger.error("Database pool error:", error); this.metrics.totalErrors++; this.emit("error", error); }); } updatePeakConnections() { const current = this.pool.totalCount; if (current > this.metrics.peakConnections) { this.metrics.peakConnections = current; } } startMonitoring() { if (this.config.healthCheckInterval > 0) { this.healthCheckTimer = setInterval(() => { this.performHealthCheck().catch((error) => { logger.error("Health check failed:", error); }); }, this.config.healthCheckInterval); } if (this.config.metricsInterval > 0) { this.metricsTimer = setInterval(() => { this.updateMetrics(); this.emit("metrics", this.getMetrics()); }, this.config.metricsInterval); } this.performHealthCheck().catch((error) => { logger.warn("Initial health check failed:", error); }); } async performHealthCheck() { const startTime = Date.now(); let client; try { this.health.totalChecks++; client = await this.pool.connect(); await client.query(this.config.healthCheckQuery); const responseTime = Date.now() - startTime; this.updateHealthMetrics(true, responseTime); logger.debug(`Health check passed in ${responseTime}ms`); } catch (error) { const responseTime = Date.now() - startTime; this.updateHealthMetrics(false, responseTime); logger.warn(`Health check failed after ${responseTime}ms:`, error); if (this.config.retryOnFailure && this.health.consecutiveFailures < this.config.maxRetries) { setTimeout(() => { this.performHealthCheck().catch(() => { }); }, this.config.retryDelayMs); } } finally { if (client) { client.release(); } } } updateHealthMetrics(success, responseTime) { this.health.lastCheck = /* @__PURE__ */ new Date(); if (success) { this.health.isHealthy = true; this.health.consecutiveFailures = 0; } else { this.health.isHealthy = false; this.health.consecutiveFailures++; this.health.totalFailures++; } const weight = Math.min(this.health.totalChecks, 10); this.health.averageResponseTime = (this.health.averageResponseTime * (weight - 1) + responseTime) / weight; } updateMetrics() { this.metrics.idleConnections = this.pool.idleCount; this.metrics.activeConnections = this.pool.totalCount - this.pool.idleCount; this.metrics.waitingRequests = this.pool.waitingCount; this.metrics.uptime = Date.now() - this.startTime.getTime(); if (this.acquireTimes.length > 0) { this.metrics.averageAcquireTime = this.acquireTimes.reduce((sum, time) => sum + time, 0) / this.acquireTimes.length; if (this.acquireTimes.length > 100) { this.acquireTimes = this.acquireTimes.slice(-100); } } } /** * Acquire a connection from the pool */ async acquire() { const startTime = Date.now(); try { const client = await this.pool.connect(); const acquireTime = Date.now() - startTime; this.acquireTimes.push(acquireTime); if (this.badConnections.has(client)) { this.badConnections.delete(client); client.release(true); return this.acquire(); } return client; } catch (error) { this.metrics.totalErrors++; logger.error("Failed to acquire connection:", error); throw new DatabaseError( "Failed to acquire database connection", ErrorCode.DB_CONNECTION_FAILED, { pool: "paradedb" }, error instanceof Error ? error : void 0 ); } } /** * Release a connection back to the pool */ release(client, error) { if (error) { client.release(true); } else { client.release(); } } /** * Mark a connection as bad (will be removed on next acquire) */ markConnectionAsBad(client) { this.badConnections.add(client); logger.warn("Connection marked as bad and will be removed"); } /** * Get current connection metrics */ getMetrics() { this.updateMetrics(); return { ...this.metrics }; } /** * Get current health status */ getHealth() { return { ...this.health }; } /** * Test connection to database */ async ping() { try { const client = await this.acquire(); await client.query("SELECT 1"); this.release(client); return true; } catch { return false; } } /** * Get pool status information */ getStatus() { return { totalCount: this.pool.totalCount, idleCount: this.pool.idleCount, waitingCount: this.pool.waitingCount, config: { min: this.config.min, max: this.config.max, idleTimeoutMillis: this.config.idleTimeoutMillis, connectionTimeoutMillis: this.config.connectionTimeoutMillis }, health: this.getHealth(), metrics: this.getMetrics() }; } /** * Close all connections and clean up */ async close() { logger.info("Closing connection pool"); if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); } if (this.metricsTimer) { clearInterval(this.metricsTimer); } await this.pool.end(); this.badConnections.clear(); this.emit("close"); logger.info("Connection pool closed"); } /** * Drain pool gracefully (wait for active connections to finish) */ async drain(timeoutMs = 3e4) { logger.info("Draining connection pool"); const startTime = Date.now(); while (this.pool.totalCount - this.pool.idleCount > 0) { if (Date.now() - startTime > timeoutMs) { logger.warn("Pool drain timeout reached, forcing close"); break; } await new Promise((resolve) => setTimeout(resolve, 100)); } await this.close(); } /** * Execute a query using a pooled connection */ async query(text, params) { const client = await this.acquire(); try { const result = await client.query(text, params); return { rows: result.rows, rowCount: result.rowCount || 0 }; } finally { this.release(client); } } /** * Execute multiple queries in a transaction */ async transaction(callback) { const client = await this.acquire(); try { await client.query("BEGIN"); const result = await callback(client); await client.query("COMMIT"); return result; } catch (error) { try { await client.query("ROLLBACK"); } catch (rollbackError) { logger.error("Transaction rollback failed:", rollbackError); this.markConnectionAsBad(client); } throw new DatabaseError( "Transaction failed", ErrorCode.DB_TRANSACTION_FAILED, { operation: "transaction" }, error instanceof Error ? error : void 0 ); } finally { this.release(client); } } } export { ConnectionPool }; //# sourceMappingURL=connection-pool.js.map