UNPKG

@yihuangdb/storage-object

Version:

A Node.js storage object layer library using Redis OM

309 lines 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionPool = void 0; const redis_1 = require("redis"); class ConnectionPool { static instance; pool = new Map(); waitQueue = []; options; cleanupInterval = null; constructor(options = {}) { // Filter out undefined values before applying defaults const cleanOptions = Object.entries(options).reduce((acc, [key, value]) => { if (value !== undefined) { acc[key] = value; } return acc; }, {}); this.options = { poolSize: 10, poolTimeout: 5000, idleTimeout: 60000, minConnections: 2, preWarm: false, ...cleanOptions, }; // Pre-warm connections if requested if (this.options.preWarm) { this.warmConnections().catch(err => { console.error('Failed to pre-warm connection pool:', err); }); } // Start cleanup interval for idle connections this.startCleanup(); } async warmConnections() { const minConns = Math.min(this.options.minConnections || 2, this.options.poolSize || 10); const promises = []; for (let i = 0; i < minConns; i++) { promises.push(this.createConnection().then(client => { const id = `pool-${Date.now()}-${Math.random().toString(36).substring(7)}`; this.pool.set(id, { client, inUse: false, lastUsed: Date.now(), id }); }).catch(err => { console.error(`Failed to pre-warm connection ${i}:`, err); })); } await Promise.all(promises); } static getInstance(options) { if (!ConnectionPool.instance) { ConnectionPool.instance = new ConnectionPool(options); } return ConnectionPool.instance; } static reset() { if (ConnectionPool.instance) { ConnectionPool.instance.shutdown().catch(err => { console.error('Error during pool reset:', err); }); ConnectionPool.instance = null; } } async createConnection() { const config = {}; if (this.options.url) { config.url = this.options.url; } else { config.socket = { host: this.options.host || 'localhost', port: this.options.port || 6379, }; } if (this.options.password) { config.password = this.options.password; } if (this.options.database !== undefined) { config.database = this.options.database; } // Add connection name for debugging config.name = this.options.connectionName || 'storage-object-pool'; // Retry strategy with exponential backoff config.socket = config.socket || {}; config.socket.reconnectStrategy = (retries) => { const maxRetries = this.options.maxRetries || 10; const delay = this.options.retryDelay || 50; if (retries > maxRetries) { return new Error('Max connection retries reached'); } // Exponential backoff with max 3s delay return Math.min(retries * delay, 3000); }; // Enable offline queue by default config.commandsQueueMaxLength = this.options.enableOfflineQueue !== false ? 1000 : 0; const client = (0, redis_1.createClient)(config); client.on('error', (err) => { console.error('Redis Pool Client Error:', err); }); await client.connect(); return client; } async acquire() { // First, try to find an idle connection for (const [id, conn] of this.pool.entries()) { if (!conn.inUse && conn.client.isOpen) { conn.inUse = true; conn.lastUsed = Date.now(); return conn.client; } } // If pool is not full, create a new connection if (this.pool.size < this.options.poolSize) { const client = await this.createConnection(); const id = `conn-${Date.now()}-${Math.random().toString(36).substring(7)}`; const conn = { client, inUse: true, lastUsed: Date.now(), id, }; this.pool.set(id, conn); return client; } // Pool is full, wait for a connection to become available return new Promise((resolve, reject) => { let timeoutHandle; const waitingRequest = { resolve: (client) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } resolve(client); }, reject: (error) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } reject(error); }, }; timeoutHandle = setTimeout(() => { const index = this.waitQueue.indexOf(waitingRequest); if (index > -1) { this.waitQueue.splice(index, 1); } reject(new Error(`Connection pool timeout after ${this.options.poolTimeout}ms`)); }, this.options.poolTimeout); this.waitQueue.push(waitingRequest); }); } release(client) { // Find the connection in the pool for (const [id, conn] of this.pool.entries()) { if (conn.client === client) { conn.inUse = false; conn.lastUsed = Date.now(); // If there are waiting requests, give them this connection if (this.waitQueue.length > 0) { const waitingRequest = this.waitQueue.shift(); if (waitingRequest) { conn.inUse = true; waitingRequest.resolve(client); } } return; } } } async warmUp(connections = 5) { // Pre-create connections to warm up the pool const warmupPromises = []; for (let i = 0; i < Math.min(connections, this.options.poolSize); i++) { warmupPromises.push(this.acquire().then(client => { // Immediately release the connection back to pool this.release(client); }).catch(err => { console.warn(`Failed to warm up connection ${i}:`, err); })); } await Promise.all(warmupPromises); } async destroy(client) { // Find and remove the connection from the pool for (const [id, conn] of this.pool.entries()) { if (conn.client === client) { this.pool.delete(id); try { if (client.isOpen) { await client.quit(); } } catch (error) { console.error('Error closing connection:', error); } return; } } } startCleanup() { if (this.cleanupInterval) { return; } // Run cleanup every 30 seconds this.cleanupInterval = setInterval(() => { const now = Date.now(); const idleTimeout = this.options.idleTimeout; for (const [id, conn] of this.pool.entries()) { // Remove idle connections that haven't been used recently if (!conn.inUse && (now - conn.lastUsed) > idleTimeout) { this.pool.delete(id); if (conn.client.isOpen) { conn.client.quit().catch(err => { console.error('Error closing idle connection:', err); }); } } } }, 30000); } /** * Get pool statistics for monitoring * @returns Pool statistics including active connections, idle connections, etc. */ getStats() { let activeConnections = 0; let idleConnections = 0; for (const conn of this.pool.values()) { if (conn.inUse) { activeConnections++; } else { idleConnections++; } } return { totalConnections: this.pool.size, activeConnections, idleConnections, waitingRequests: this.waitQueue.length, poolSize: this.options.poolSize, }; } /** * Check if the pool is healthy * @returns true if pool is operating normally */ isHealthy() { const stats = this.getStats(); return stats.totalConnections > 0 && stats.waitingRequests < stats.poolSize; } /** * Update pool options dynamically * @param options - New pool options to apply */ updateOptions(options) { this.options = { ...this.options, ...options }; } /** * Force cleanup of idle connections */ forceCleanup() { const now = Date.now(); const idleTimeout = 0; // Force immediate cleanup for (const [id, conn] of this.pool.entries()) { if (!conn.inUse) { this.pool.delete(id); if (conn.client.isOpen) { conn.client.quit().catch(err => { console.error('Error closing idle connection during forced cleanup:', err); }); } } } } async shutdown() { // Stop cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } // Clear wait queue and reject all waiting requests const waitingRequests = [...this.waitQueue]; this.waitQueue = []; // Reject all waiting promises waitingRequests.forEach(request => { request.reject(new Error('Connection pool shutting down')); }); // Close all connections const closePromises = []; for (const [id, conn] of this.pool.entries()) { if (conn.client.isOpen) { closePromises.push(conn.client.quit().then(() => { // Successfully closed }).catch(err => { console.error('Error closing connection during shutdown:', err); })); } } await Promise.all(closePromises); this.pool.clear(); } } exports.ConnectionPool = ConnectionPool; //# sourceMappingURL=connection-pool.js.map