2025-07-15 16:24:31 -06:00
|
|
|
const IoRedis = require('ioredis');
|
2025-07-25 12:33:05 -04:00
|
|
|
const { logger } = require('@librechat/data-schemas');
|
2025-07-15 16:24:31 -06:00
|
|
|
const { createClient, createCluster } = require('@keyv/redis');
|
2025-07-25 12:33:05 -04:00
|
|
|
const { cacheConfig } = require('./cacheConfig');
|
2025-07-15 16:24:31 -06:00
|
|
|
|
|
|
|
const GLOBAL_PREFIX_SEPARATOR = '::';
|
|
|
|
|
|
|
|
const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri));
|
|
|
|
const username = urls?.[0].username || cacheConfig.REDIS_USERNAME;
|
|
|
|
const password = urls?.[0].password || cacheConfig.REDIS_PASSWORD;
|
|
|
|
const ca = cacheConfig.REDIS_CA;
|
|
|
|
|
|
|
|
/** @type {import('ioredis').Redis | import('ioredis').Cluster | null} */
|
|
|
|
let ioredisClient = null;
|
|
|
|
if (cacheConfig.USE_REDIS) {
|
2025-07-28 14:21:39 -04:00
|
|
|
/** @type {import('ioredis').RedisOptions | import('ioredis').ClusterOptions} */
|
2025-07-15 16:24:31 -06:00
|
|
|
const redisOptions = {
|
|
|
|
username: username,
|
|
|
|
password: password,
|
|
|
|
tls: ca ? { ca } : undefined,
|
|
|
|
keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${GLOBAL_PREFIX_SEPARATOR}`,
|
|
|
|
maxListeners: cacheConfig.REDIS_MAX_LISTENERS,
|
2025-07-28 14:21:39 -04:00
|
|
|
retryStrategy: (times) => {
|
|
|
|
if (
|
|
|
|
cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
|
|
|
|
times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS
|
|
|
|
) {
|
|
|
|
logger.error(
|
|
|
|
`ioredis giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`,
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const delay = Math.min(times * 50, cacheConfig.REDIS_RETRY_MAX_DELAY);
|
|
|
|
logger.info(`ioredis reconnecting... attempt ${times}, delay ${delay}ms`);
|
|
|
|
return delay;
|
|
|
|
},
|
|
|
|
reconnectOnError: (err) => {
|
|
|
|
const targetError = 'READONLY';
|
|
|
|
if (err.message.includes(targetError)) {
|
|
|
|
logger.warn('ioredis reconnecting due to READONLY error');
|
2025-08-12 20:23:29 -06:00
|
|
|
return 2; // Return retry delay instead of boolean
|
2025-07-28 14:21:39 -04:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE,
|
|
|
|
connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT,
|
|
|
|
maxRetriesPerRequest: 3,
|
2025-07-15 16:24:31 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
ioredisClient =
|
2025-08-16 19:41:53 +01:00
|
|
|
urls.length === 1 && !cacheConfig.USE_REDIS_CLUSTER
|
2025-07-15 16:24:31 -06:00
|
|
|
? new IoRedis(cacheConfig.REDIS_URI, redisOptions)
|
2025-08-12 20:23:29 -06:00
|
|
|
: new IoRedis.Cluster(
|
|
|
|
urls.map((url) => ({ host: url.hostname, port: parseInt(url.port, 10) || 6379 })),
|
|
|
|
{
|
2025-08-27 16:09:07 -04:00
|
|
|
...(cacheConfig.REDIS_USE_ALTERNATIVE_DNS_LOOKUP
|
|
|
|
? { dnsLookup: (address, callback) => callback(null, address) }
|
|
|
|
: {}),
|
2025-08-12 20:23:29 -06:00
|
|
|
redisOptions,
|
|
|
|
clusterRetryStrategy: (times) => {
|
|
|
|
if (
|
|
|
|
cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
|
|
|
|
times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS
|
|
|
|
) {
|
|
|
|
logger.error(
|
|
|
|
`ioredis cluster giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`,
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const delay = Math.min(times * 100, cacheConfig.REDIS_RETRY_MAX_DELAY);
|
|
|
|
logger.info(`ioredis cluster reconnecting... attempt ${times}, delay ${delay}ms`);
|
|
|
|
return delay;
|
|
|
|
},
|
|
|
|
enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE,
|
2025-07-28 14:21:39 -04:00
|
|
|
},
|
2025-08-12 20:23:29 -06:00
|
|
|
);
|
2025-07-15 16:24:31 -06:00
|
|
|
|
2025-07-25 12:33:05 -04:00
|
|
|
ioredisClient.on('error', (err) => {
|
|
|
|
logger.error('ioredis client error:', err);
|
|
|
|
});
|
|
|
|
|
2025-07-28 14:21:39 -04:00
|
|
|
ioredisClient.on('connect', () => {
|
|
|
|
logger.info('ioredis client connected');
|
|
|
|
});
|
|
|
|
|
|
|
|
ioredisClient.on('ready', () => {
|
|
|
|
logger.info('ioredis client ready');
|
|
|
|
});
|
|
|
|
|
|
|
|
ioredisClient.on('reconnecting', (delay) => {
|
|
|
|
logger.info(`ioredis client reconnecting in ${delay}ms`);
|
|
|
|
});
|
|
|
|
|
|
|
|
ioredisClient.on('close', () => {
|
|
|
|
logger.warn('ioredis client connection closed');
|
|
|
|
});
|
|
|
|
|
2025-07-25 12:33:05 -04:00
|
|
|
/** Ping Interval to keep the Redis server connection alive (if enabled) */
|
2025-07-25 09:00:02 -06:00
|
|
|
let pingInterval = null;
|
2025-07-25 12:33:05 -04:00
|
|
|
const clearPingInterval = () => {
|
|
|
|
if (pingInterval) {
|
|
|
|
clearInterval(pingInterval);
|
|
|
|
pingInterval = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-07-25 09:00:02 -06:00
|
|
|
if (cacheConfig.REDIS_PING_INTERVAL > 0) {
|
2025-07-25 12:33:05 -04:00
|
|
|
pingInterval = setInterval(() => {
|
|
|
|
if (ioredisClient && ioredisClient.status === 'ready') {
|
2025-07-28 14:21:39 -04:00
|
|
|
ioredisClient.ping().catch((err) => {
|
|
|
|
logger.error('ioredis ping failed:', err);
|
|
|
|
});
|
2025-07-25 12:33:05 -04:00
|
|
|
}
|
|
|
|
}, cacheConfig.REDIS_PING_INTERVAL * 1000);
|
|
|
|
ioredisClient.on('close', clearPingInterval);
|
|
|
|
ioredisClient.on('end', clearPingInterval);
|
2025-07-25 09:00:02 -06:00
|
|
|
}
|
2025-07-15 16:24:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {import('@keyv/redis').RedisClient | import('@keyv/redis').RedisCluster | null} */
|
|
|
|
let keyvRedisClient = null;
|
|
|
|
if (cacheConfig.USE_REDIS) {
|
2025-07-25 12:33:05 -04:00
|
|
|
/**
|
|
|
|
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
|
|
|
|
* The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js
|
2025-07-28 14:21:39 -04:00
|
|
|
* @type {import('@keyv/redis').RedisClientOptions | import('@keyv/redis').RedisClusterOptions}
|
2025-07-25 12:33:05 -04:00
|
|
|
*/
|
2025-07-28 14:21:39 -04:00
|
|
|
const redisOptions = {
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
socket: {
|
|
|
|
tls: ca != null,
|
|
|
|
ca,
|
|
|
|
connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT,
|
|
|
|
reconnectStrategy: (retries) => {
|
|
|
|
if (
|
|
|
|
cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 &&
|
|
|
|
retries > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS
|
|
|
|
) {
|
|
|
|
logger.error(
|
|
|
|
`@keyv/redis client giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`,
|
|
|
|
);
|
|
|
|
return new Error('Max reconnection attempts reached');
|
|
|
|
}
|
|
|
|
const delay = Math.min(retries * 100, cacheConfig.REDIS_RETRY_MAX_DELAY);
|
|
|
|
logger.info(`@keyv/redis reconnecting... attempt ${retries}, delay ${delay}ms`);
|
|
|
|
return delay;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
disableOfflineQueue: !cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE,
|
|
|
|
};
|
2025-07-15 16:24:31 -06:00
|
|
|
|
|
|
|
keyvRedisClient =
|
2025-08-16 19:41:53 +01:00
|
|
|
urls.length === 1 && !cacheConfig.USE_REDIS_CLUSTER
|
2025-07-15 16:24:31 -06:00
|
|
|
? createClient({ url: cacheConfig.REDIS_URI, ...redisOptions })
|
|
|
|
: createCluster({
|
2025-08-12 20:23:29 -06:00
|
|
|
rootNodes: urls.map((url) => ({ url: url.href })),
|
2025-07-15 16:24:31 -06:00
|
|
|
defaults: redisOptions,
|
|
|
|
});
|
|
|
|
|
|
|
|
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
|
|
|
|
|
2025-07-25 12:33:05 -04:00
|
|
|
keyvRedisClient.on('error', (err) => {
|
|
|
|
logger.error('@keyv/redis client error:', err);
|
|
|
|
});
|
|
|
|
|
2025-07-28 14:21:39 -04:00
|
|
|
keyvRedisClient.on('connect', () => {
|
|
|
|
logger.info('@keyv/redis client connected');
|
|
|
|
});
|
|
|
|
|
|
|
|
keyvRedisClient.on('ready', () => {
|
|
|
|
logger.info('@keyv/redis client ready');
|
|
|
|
});
|
|
|
|
|
|
|
|
keyvRedisClient.on('reconnecting', () => {
|
|
|
|
logger.info('@keyv/redis client reconnecting...');
|
|
|
|
});
|
|
|
|
|
|
|
|
keyvRedisClient.on('disconnect', () => {
|
|
|
|
logger.warn('@keyv/redis client disconnected');
|
|
|
|
});
|
|
|
|
|
|
|
|
keyvRedisClient.connect().catch((err) => {
|
|
|
|
logger.error('@keyv/redis initial connection failed:', err);
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
|
2025-07-25 12:33:05 -04:00
|
|
|
/** Ping Interval to keep the Redis server connection alive (if enabled) */
|
2025-07-25 09:00:02 -06:00
|
|
|
let pingInterval = null;
|
2025-07-25 12:33:05 -04:00
|
|
|
const clearPingInterval = () => {
|
|
|
|
if (pingInterval) {
|
|
|
|
clearInterval(pingInterval);
|
|
|
|
pingInterval = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-07-25 09:00:02 -06:00
|
|
|
if (cacheConfig.REDIS_PING_INTERVAL > 0) {
|
2025-07-25 12:33:05 -04:00
|
|
|
pingInterval = setInterval(() => {
|
|
|
|
if (keyvRedisClient && keyvRedisClient.isReady) {
|
2025-07-28 14:21:39 -04:00
|
|
|
keyvRedisClient.ping().catch((err) => {
|
|
|
|
logger.error('@keyv/redis ping failed:', err);
|
|
|
|
});
|
2025-07-25 12:33:05 -04:00
|
|
|
}
|
|
|
|
}, cacheConfig.REDIS_PING_INTERVAL * 1000);
|
|
|
|
keyvRedisClient.on('disconnect', clearPingInterval);
|
|
|
|
keyvRedisClient.on('end', clearPingInterval);
|
2025-07-25 09:00:02 -06:00
|
|
|
}
|
2025-07-15 16:24:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = { ioredisClient, keyvRedisClient, GLOBAL_PREFIX_SEPARATOR };
|