import Redis from "ioredis"; import { logger } from "@plane/logger"; import { env } from "./env"; export class RedisManager { private static instance: RedisManager; private redisClient: Redis | null = null; private isConnected: boolean = false; private connectionPromise: Promise | null = null; private constructor() {} public static getInstance(): RedisManager { if (!RedisManager.instance) { RedisManager.instance = new RedisManager(); } return RedisManager.instance; } public async initialize(): Promise { if (this.redisClient && this.isConnected) { logger.info("Redis client already initialized and connected"); return; } if (this.connectionPromise) { logger.info("Redis connection already in progress, waiting..."); await this.connectionPromise; return; } this.connectionPromise = this.connect(); await this.connectionPromise; } private getRedisUrl(): string { const redisUrl = env.REDIS_URL; const redisHost = env.REDIS_HOST; const redisPort = env.REDIS_PORT; if (redisUrl) { return redisUrl; } if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) { return `redis://${redisHost}:${redisPort}`; } return ""; } private async connect(): Promise { try { const redisUrl = this.getRedisUrl(); if (!redisUrl) { logger.warn("No Redis URL provided, Redis functionality will be disabled"); this.isConnected = false; return; } this.redisClient = new Redis(redisUrl, { lazyConnect: true, keepAlive: 30000, connectTimeout: 10000, commandTimeout: 5000, // enableOfflineQueue: false, maxRetriesPerRequest: 3, }); // Set up event listeners this.redisClient.on("connect", () => { logger.info("Redis client connected"); this.isConnected = true; }); this.redisClient.on("ready", () => { logger.info("Redis client ready"); this.isConnected = true; }); this.redisClient.on("error", (error) => { logger.error("Redis client error:", error); this.isConnected = false; }); this.redisClient.on("close", () => { logger.warn("Redis client connection closed"); this.isConnected = false; }); this.redisClient.on("reconnecting", () => { logger.info("Redis client reconnecting..."); this.isConnected = false; }); // Connect to Redis await this.redisClient.connect(); // Test the connection await this.redisClient.ping(); logger.info("Redis connection test successful"); } catch (error) { logger.error("Failed to initialize Redis client:", error); this.isConnected = false; throw error; } finally { this.connectionPromise = null; } } public getClient(): Redis | null { if (!this.redisClient || !this.isConnected) { logger.warn("Redis client not available or not connected"); return null; } return this.redisClient; } public isClientConnected(): boolean { return this.isConnected && this.redisClient !== null; } public async disconnect(): Promise { if (this.redisClient) { try { await this.redisClient.quit(); logger.info("Redis client disconnected gracefully"); } catch (error) { logger.error("Error disconnecting Redis client:", error); // Force disconnect if quit fails this.redisClient.disconnect(); } finally { this.redisClient = null; this.isConnected = false; } } } // Convenience methods for common Redis operations public async set(key: string, value: string, ttl?: number): Promise { const client = this.getClient(); if (!client) return false; try { if (ttl) { await client.setex(key, ttl, value); } else { await client.set(key, value); } return true; } catch (error) { logger.error(`Error setting Redis key ${key}:`, error); return false; } } public async get(key: string): Promise { const client = this.getClient(); if (!client) return null; try { return await client.get(key); } catch (error) { logger.error(`Error getting Redis key ${key}:`, error); return null; } } public async del(key: string): Promise { const client = this.getClient(); if (!client) return false; try { await client.del(key); return true; } catch (error) { logger.error(`Error deleting Redis key ${key}:`, error); return false; } } public async exists(key: string): Promise { const client = this.getClient(); if (!client) return false; try { const result = await client.exists(key); return result === 1; } catch (error) { logger.error(`Error checking Redis key ${key}:`, error); return false; } } public async expire(key: string, ttl: number): Promise { const client = this.getClient(); if (!client) return false; try { const result = await client.expire(key, ttl); return result === 1; } catch (error) { logger.error(`Error setting expiry for Redis key ${key}:`, error); return false; } } } // Export a default instance for convenience export const redisManager = RedisManager.getInstance();