const Redis = require('ioredis'); const logger = require('./logger'); /** * Redis Connection Configuration * Supports both single instance and cluster modes */ const redisConfig = { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT) || 6379, password: process.env.REDIS_PASSWORD || undefined, db: parseInt(process.env.REDIS_DB) || 0, retryStrategy: (times) => { // Stop retrying after 3 attempts in development if (process.env.NODE_ENV === 'development' && times > 3) { logger.info('Redis unavailable - caching disabled (optional feature)'); return null; // Stop retrying } const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3, enableReadyCheck: true, enableOfflineQueue: true, lazyConnect: true, // Don't connect immediately connectTimeout: 5000, // Reduced timeout keepAlive: 30000, family: 4, // IPv4 // Connection pool settings minReconnectInterval: 100, maxReconnectInterval: 3000, // Reduce logging noise showFriendlyErrorStack: process.env.NODE_ENV !== 'development' }; // Create Redis client let redisClient = null; let isConnected = false; try { redisClient = new Redis(redisConfig); // Attempt initial connection redisClient.connect().catch(() => { // Silently fail if Redis is not available in development if (process.env.NODE_ENV === 'development') { logger.info('Redis not available - continuing without cache (optional)'); } }); // Connection events redisClient.on('connect', () => { logger.info('Redis client connecting...'); }); redisClient.on('ready', () => { isConnected = true; logger.info('Redis client connected and ready'); }); redisClient.on('error', (err) => { isConnected = false; // Only log errors in production or first error if (process.env.NODE_ENV === 'production' || !errorLogged) { logger.error('Redis client error:', err.message || err); errorLogged = true; } }); redisClient.on('close', () => { if (isConnected) { isConnected = false; logger.warn('Redis client connection closed'); } }); redisClient.on('reconnecting', () => { // Only log once if (isConnected === false) { logger.info('Redis client reconnecting...'); } }); redisClient.on('end', () => { if (isConnected) { isConnected = false; logger.info('Redis connection ended'); } }); } catch (error) { logger.error('Failed to create Redis client:', error); } // Track if error has been logged let errorLogged = false; /** * Check if Redis is connected */ const isRedisConnected = () => { return isConnected && redisClient && redisClient.status === 'ready'; }; /** * Get Redis client */ const getRedisClient = () => { if (!isRedisConnected()) { logger.warn('Redis client not connected'); return null; } return redisClient; }; /** * Close Redis connection gracefully */ const closeRedis = async () => { if (redisClient) { await redisClient.quit(); logger.info('Redis connection closed'); } }; /** * Cache helper functions */ /** * Get cached data * @param {string} key - Cache key * @returns {Promise} - Parsed JSON data or null */ const getCache = async (key) => { try { if (!isRedisConnected()) { logger.warn('Redis not connected, cache miss'); return null; } const data = await redisClient.get(key); if (!data) return null; logger.debug(`Cache hit: ${key}`); return JSON.parse(data); } catch (error) { logger.error(`Cache get error for key ${key}:`, error); return null; } }; /** * Set cached data * @param {string} key - Cache key * @param {any} value - Data to cache * @param {number} ttl - Time to live in seconds (default: 300 = 5 minutes) * @returns {Promise} - Success status */ const setCache = async (key, value, ttl = 300) => { try { if (!isRedisConnected()) { logger.warn('Redis not connected, skipping cache set'); return false; } const serialized = JSON.stringify(value); await redisClient.setex(key, ttl, serialized); logger.debug(`Cache set: ${key} (TTL: ${ttl}s)`); return true; } catch (error) { logger.error(`Cache set error for key ${key}:`, error); return false; } }; /** * Delete cached data * @param {string} key - Cache key or pattern * @returns {Promise} - Success status */ const deleteCache = async (key) => { try { if (!isRedisConnected()) { return false; } // Support pattern deletion (e.g., "user:*") if (key.includes('*')) { const keys = await redisClient.keys(key); if (keys.length > 0) { await redisClient.del(...keys); logger.debug(`Cache deleted: ${keys.length} keys matching ${key}`); } } else { await redisClient.del(key); logger.debug(`Cache deleted: ${key}`); } return true; } catch (error) { logger.error(`Cache delete error for key ${key}:`, error); return false; } }; /** * Clear all cache * @returns {Promise} - Success status */ const clearCache = async () => { try { if (!isRedisConnected()) { return false; } await redisClient.flushdb(); logger.info('All cache cleared'); return true; } catch (error) { logger.error('Cache clear error:', error); return false; } }; /** * Get multiple keys at once * @param {string[]} keys - Array of cache keys * @returns {Promise} - Object with key-value pairs */ const getCacheMultiple = async (keys) => { try { if (!isRedisConnected() || !keys || keys.length === 0) { return {}; } const values = await redisClient.mget(...keys); const result = {}; keys.forEach((key, index) => { if (values[index]) { try { result[key] = JSON.parse(values[index]); } catch (err) { result[key] = null; } } else { result[key] = null; } }); return result; } catch (error) { logger.error('Cache mget error:', error); return {}; } }; /** * Increment a counter * @param {string} key - Cache key * @param {number} increment - Amount to increment (default: 1) * @param {number} ttl - Time to live in seconds (optional) * @returns {Promise} - New value */ const incrementCache = async (key, increment = 1, ttl = null) => { try { if (!isRedisConnected()) { return 0; } const newValue = await redisClient.incrby(key, increment); if (ttl) { await redisClient.expire(key, ttl); } return newValue; } catch (error) { logger.error(`Cache increment error for key ${key}:`, error); return 0; } }; /** * Check if key exists * @param {string} key - Cache key * @returns {Promise} - Exists status */ const cacheExists = async (key) => { try { if (!isRedisConnected()) { return false; } const exists = await redisClient.exists(key); return exists === 1; } catch (error) { logger.error(`Cache exists error for key ${key}:`, error); return false; } }; module.exports = { redisClient, isRedisConnected, getRedisClient, closeRedis, getCache, setCache, deleteCache, clearCache, getCacheMultiple, incrementCache, cacheExists };