@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
JavaScript
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