import Redis from 'ioredis'; import { getLogger } from '@stock-bot/logger'; import { dragonflyConfig } from '@stock-bot/config'; interface ConnectionConfig { name: string; singleton?: boolean; db?: number; } /** * Redis Connection Manager for managing shared and unique connections */ export class RedisConnectionManager { private connections = new Map(); private static sharedConnections = new Map(); private static instance: RedisConnectionManager; private logger = getLogger('redis-connection-manager'); // Singleton pattern for the manager itself static getInstance(): RedisConnectionManager { if (!this.instance) { this.instance = new RedisConnectionManager(); } return this.instance; } /** * Get or create a Redis connection * @param config Connection configuration * @returns Redis connection instance */ getConnection(config: ConnectionConfig): Redis { const { name, singleton = false, db } = config; if (singleton) { // Use shared connection across all instances if (!RedisConnectionManager.sharedConnections.has(name)) { const connection = this.createConnection(name, db); RedisConnectionManager.sharedConnections.set(name, connection); this.logger.info(`Created shared Redis connection: ${name}`); } return RedisConnectionManager.sharedConnections.get(name)!; } else { // Create unique connection per instance const uniqueName = `${name}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const connection = this.createConnection(uniqueName, db); this.connections.set(uniqueName, connection); this.logger.info(`Created unique Redis connection: ${uniqueName}`); return connection; } } /** * Create a new Redis connection with configuration */ private createConnection(name: string, db?: number): Redis { const redisConfig = { host: dragonflyConfig.DRAGONFLY_HOST, port: dragonflyConfig.DRAGONFLY_PORT, password: dragonflyConfig.DRAGONFLY_PASSWORD || undefined, username: dragonflyConfig.DRAGONFLY_USERNAME || undefined, db: db ?? dragonflyConfig.DRAGONFLY_DATABASE, maxRetriesPerRequest: dragonflyConfig.DRAGONFLY_MAX_RETRIES, retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY, connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT, commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT, keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE ? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000 : 0, connectionName: name, lazyConnect: true, ...(dragonflyConfig.DRAGONFLY_TLS && { tls: { cert: dragonflyConfig.DRAGONFLY_TLS_CERT_FILE || undefined, key: dragonflyConfig.DRAGONFLY_TLS_KEY_FILE || undefined, ca: dragonflyConfig.DRAGONFLY_TLS_CA_FILE || undefined, rejectUnauthorized: !dragonflyConfig.DRAGONFLY_TLS_SKIP_VERIFY, }, }), }; const redis = new Redis(redisConfig); // Setup event handlers redis.on('connect', () => { this.logger.info(`Redis connection established: ${name}`); }); redis.on('ready', () => { this.logger.info(`Redis connection ready: ${name}`); }); redis.on('error', (err) => { this.logger.error(`Redis connection error for ${name}:`, err); }); redis.on('close', () => { this.logger.info(`Redis connection closed: ${name}`); }); redis.on('reconnecting', () => { this.logger.info(`Redis reconnecting: ${name}`); }); return redis; } /** * Close a specific connection */ async closeConnection(connection: Redis): Promise { try { await connection.quit(); } catch (error) { this.logger.error('Error closing Redis connection:', error); } } /** * Close all connections managed by this instance */ async closeAllConnections(): Promise { // Close instance-specific connections const instancePromises = Array.from(this.connections.values()).map(conn => this.closeConnection(conn) ); await Promise.all(instancePromises); this.connections.clear(); // Close shared connections (only if this is the last instance) if (RedisConnectionManager.instance === this) { const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(conn => this.closeConnection(conn) ); await Promise.all(sharedPromises); RedisConnectionManager.sharedConnections.clear(); } this.logger.info('All Redis connections closed'); } /** * Get connection statistics */ getConnectionCount(): { shared: number; unique: number } { return { shared: RedisConnectionManager.sharedConnections.size, unique: this.connections.size }; } /** * Get all connection names for monitoring */ getConnectionNames(): { shared: string[]; unique: string[] } { return { shared: Array.from(RedisConnectionManager.sharedConnections.keys()), unique: Array.from(this.connections.keys()) }; } /** * Health check for all connections */ async healthCheck(): Promise<{ healthy: boolean; details: Record }> { const details: Record = {}; let allHealthy = true; // Check shared connections for (const [name, connection] of RedisConnectionManager.sharedConnections) { try { await connection.ping(); details[`shared:${name}`] = true; } catch (error) { details[`shared:${name}`] = false; allHealthy = false; } } // Check instance connections for (const [name, connection] of this.connections) { try { await connection.ping(); details[`unique:${name}`] = true; } catch (error) { details[`unique:${name}`] = false; allHealthy = false; } } return { healthy: allHealthy, details }; } } export default RedisConnectionManager;