From 3d1cedb85b782eef64e4b0795ec799109c373392 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 12 Oct 2025 11:15:18 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A1=20refactor:=20Flush=20Redis=20Cach?= =?UTF-8?q?e=20Script=20(#10087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 feat: Enhance Redis Configuration and Connection Handling in Cache Flush Utility - Added support for Redis username, password, and CA certificate. - Improved Redis client creation to handle both cluster and single instance configurations. - Implemented a helper function to read the Redis CA certificate with error handling. - Updated connection logic to include timeout and error handling for Redis connections. * refactor: flush cache if redis URI is defined --- config/flush-cache.js | 99 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/config/flush-cache.js b/config/flush-cache.js index b65339ad18..07c744ca4e 100644 --- a/config/flush-cache.js +++ b/config/flush-cache.js @@ -18,11 +18,38 @@ const fs = require('fs'); // Set up environment require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); -const { USE_REDIS, REDIS_URI, REDIS_KEY_PREFIX } = process.env; +const { + USE_REDIS, + REDIS_URI, + REDIS_USERNAME, + REDIS_PASSWORD, + REDIS_CA, + REDIS_KEY_PREFIX, + USE_REDIS_CLUSTER, + REDIS_USE_ALTERNATIVE_DNS_LOOKUP, +} = process.env; // Simple utility function const isEnabled = (value) => value === 'true' || value === true; +// Helper function to read Redis CA certificate +const getRedisCA = () => { + if (!REDIS_CA) { + return null; + } + try { + if (fs.existsSync(REDIS_CA)) { + return fs.readFileSync(REDIS_CA, 'utf8'); + } else { + console.warn(`⚠️ Redis CA certificate file not found: ${REDIS_CA}`); + return null; + } + } catch (error) { + console.error(`❌ Failed to read Redis CA certificate file '${REDIS_CA}':`, error.message); + return null; + } +}; + async function showHelp() { console.log(` LibreChat Cache Flush Utility @@ -67,21 +94,67 @@ async function flushRedisCache(dryRun = false, verbose = false) { console.log(` Prefix: ${REDIS_KEY_PREFIX || 'None'}`); } - // Create Redis client directly - const Redis = require('ioredis'); + // Create Redis client using same pattern as main app + const IoRedis = require('ioredis'); let redis; - // Handle cluster vs single Redis - if (process.env.USE_REDIS_CLUSTER === 'true') { - const hosts = REDIS_URI.split(',').map((uri) => { - const [host, port] = uri.split(':'); - return { host, port: parseInt(port) || 6379 }; - }); - redis = new Redis.Cluster(hosts); + // Parse credentials from URI or use environment variables (same as redisClients.ts) + const urls = (REDIS_URI || '').split(',').map((uri) => new URL(uri)); + const username = urls[0]?.username || REDIS_USERNAME; + const password = urls[0]?.password || REDIS_PASSWORD; + const ca = getRedisCA(); + + // Redis options (matching redisClients.ts configuration) + const redisOptions = { + username: username, + password: password, + tls: ca ? { ca } : undefined, + connectTimeout: 10000, + maxRetriesPerRequest: 3, + enableOfflineQueue: true, + lazyConnect: false, + }; + + // Handle cluster vs single Redis (same logic as redisClients.ts) + const useCluster = urls.length > 1 || isEnabled(USE_REDIS_CLUSTER); + + if (useCluster) { + const clusterOptions = { + redisOptions, + enableOfflineQueue: true, + }; + + // Add DNS lookup for AWS ElastiCache if needed (same as redisClients.ts) + if (isEnabled(REDIS_USE_ALTERNATIVE_DNS_LOOKUP)) { + clusterOptions.dnsLookup = (address, callback) => callback(null, address); + } + + redis = new IoRedis.Cluster( + urls.map((url) => ({ host: url.hostname, port: parseInt(url.port, 10) || 6379 })), + clusterOptions, + ); } else { - redis = new Redis(REDIS_URI); + // @ts-ignore - ioredis default export is constructable despite linter warning + redis = new IoRedis(REDIS_URI, redisOptions); } + // Wait for connection + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Connection timeout')); + }, 10000); + + redis.once('ready', () => { + clearTimeout(timeout); + resolve(undefined); + }); + + redis.once('error', (err) => { + clearTimeout(timeout); + reject(err); + }); + }); + if (dryRun) { console.log('🔍 [DRY RUN] Would flush Redis cache'); try { @@ -105,7 +178,7 @@ async function flushRedisCache(dryRun = false, verbose = false) { try { const keys = await redis.keys('*'); keyCount = keys.length; - } catch (error) { + } catch (_error) { // Continue with flush even if we can't count keys } @@ -209,7 +282,7 @@ async function main() { } let success = true; - const isRedisEnabled = isEnabled(USE_REDIS) && REDIS_URI; + const isRedisEnabled = isEnabled(USE_REDIS) || (REDIS_URI != null && REDIS_URI !== ''); // Flush the appropriate cache type if (isRedisEnabled) {