269 lines
8.1 KiB
TypeScript
269 lines
8.1 KiB
TypeScript
import Redis from 'ioredis';
|
|
import { getLogger } from '@stock-bot/logger';
|
|
import type { RedisConfig } from './types';
|
|
|
|
interface ConnectionConfig {
|
|
name: string;
|
|
singleton?: boolean;
|
|
db?: number;
|
|
redisConfig: RedisConfig;
|
|
}
|
|
|
|
/**
|
|
* Redis Connection Manager for managing shared and unique connections
|
|
*/
|
|
export class RedisConnectionManager {
|
|
private connections = new Map<string, Redis>();
|
|
private static sharedConnections = new Map<string, Redis>();
|
|
private static instance: RedisConnectionManager;
|
|
private logger = getLogger('redis-connection-manager');
|
|
private static readyConnections = new Set<string>();
|
|
|
|
// 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, redisConfig } = config;
|
|
|
|
if (singleton) {
|
|
// Use shared connection across all instances
|
|
if (!RedisConnectionManager.sharedConnections.has(name)) {
|
|
const connection = this.createConnection(name, redisConfig, 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, redisConfig, 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, config: RedisConfig, db?: number): Redis {
|
|
const redisOptions = {
|
|
host: config.host,
|
|
port: config.port,
|
|
password: config.password || undefined,
|
|
username: config.username || undefined,
|
|
db: db ?? config.db ?? 0,
|
|
maxRetriesPerRequest: config.maxRetriesPerRequest ?? 3,
|
|
retryDelayOnFailover: config.retryDelayOnFailover ?? 100,
|
|
connectTimeout: config.connectTimeout ?? 10000,
|
|
commandTimeout: config.commandTimeout ?? 5000,
|
|
keepAlive: config.keepAlive ?? 0,
|
|
connectionName: name,
|
|
lazyConnect: false, // Connect immediately instead of waiting for first command
|
|
...(config.tls && {
|
|
tls: {
|
|
cert: config.tls.cert || undefined,
|
|
key: config.tls.key || undefined,
|
|
ca: config.tls.ca || undefined,
|
|
rejectUnauthorized: config.tls.rejectUnauthorized ?? true,
|
|
},
|
|
}),
|
|
};
|
|
|
|
const redis = new Redis(redisOptions);
|
|
|
|
// 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<void> {
|
|
try {
|
|
await connection.quit();
|
|
} catch (error) {
|
|
this.logger.error('Error closing Redis connection:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close all connections managed by this instance
|
|
*/
|
|
async closeAllConnections(): Promise<void> {
|
|
// 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<string, boolean> }> {
|
|
const details: Record<string, boolean> = {};
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Wait for all created connections to be ready
|
|
* @param timeout Maximum time to wait in milliseconds
|
|
* @returns Promise that resolves when all connections are ready
|
|
*/
|
|
static async waitForAllConnections(timeout: number = 30000): Promise<void> {
|
|
const instance = this.getInstance();
|
|
const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
|
|
|
|
if (allConnections.size === 0) {
|
|
instance.logger.info('No Redis connections to wait for');
|
|
return;
|
|
}
|
|
|
|
instance.logger.info(`Waiting for ${allConnections.size} Redis connections to be ready...`);
|
|
|
|
const connectionPromises = Array.from(allConnections.entries()).map(([name, redis]) =>
|
|
instance.waitForConnection(redis, name, timeout)
|
|
);
|
|
|
|
try {
|
|
await Promise.all(connectionPromises);
|
|
instance.logger.info('All Redis connections are ready');
|
|
} catch (error) {
|
|
instance.logger.error('Failed to establish all Redis connections:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a specific connection to be ready
|
|
*/
|
|
private async waitForConnection(redis: Redis, name: string, timeout: number): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const timeoutId = setTimeout(() => {
|
|
reject(new Error(`Redis connection ${name} failed to be ready within ${timeout}ms`));
|
|
}, timeout);
|
|
|
|
const onReady = () => {
|
|
clearTimeout(timeoutId);
|
|
RedisConnectionManager.readyConnections.add(name);
|
|
this.logger.info(`Redis connection ready: ${name}`);
|
|
resolve();
|
|
};
|
|
|
|
const onError = (err: Error) => {
|
|
clearTimeout(timeoutId);
|
|
this.logger.error(`Redis connection failed for ${name}:`, err);
|
|
reject(err);
|
|
};
|
|
|
|
if (redis.status === 'ready') {
|
|
onReady();
|
|
} else {
|
|
redis.once('ready', onReady);
|
|
redis.once('error', onError);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if all connections are ready
|
|
*/
|
|
static areAllConnectionsReady(): boolean {
|
|
const instance = this.getInstance();
|
|
const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
|
|
|
|
return (
|
|
allConnections.size > 0 &&
|
|
Array.from(allConnections.keys()).every(name => this.readyConnections.has(name))
|
|
);
|
|
}
|
|
}
|
|
|
|
export default RedisConnectionManager;
|