👑 feat: Distributed Leader Election with Redis for Multi-instance Coordination (#10189)

* 🔧 refactor: Move GLOBAL_PREFIX_SEPARATOR to cacheConfig for consistency

* 👑 feat: Implement distributed leader election using Redis
This commit is contained in:
Theo N. Truong 2025-10-30 15:08:04 -06:00 committed by GitHub
parent 1e53ffa7ea
commit 8f4705f683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 452 additions and 15 deletions

View file

@ -1,11 +1,14 @@
import type { Keyv } from 'keyv';
// Mock GLOBAL_PREFIX_SEPARATOR
jest.mock('../../redisClients', () => {
const originalModule = jest.requireActual('../../redisClients');
// Mock GLOBAL_PREFIX_SEPARATOR from cacheConfig
jest.mock('../../cacheConfig', () => {
const originalModule = jest.requireActual('../../cacheConfig');
return {
...originalModule,
GLOBAL_PREFIX_SEPARATOR: '>>',
cacheConfig: {
...originalModule.cacheConfig,
GLOBAL_PREFIX_SEPARATOR: '>>',
},
};
});

View file

@ -65,6 +65,7 @@ const cacheConfig = {
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CA: getRedisCA(),
REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR ?? ''] || REDIS_KEY_PREFIX || '',
GLOBAL_PREFIX_SEPARATOR: '::',
REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40),
REDIS_PING_INTERVAL: math(process.env.REDIS_PING_INTERVAL, 0),
/** Max delay between reconnection attempts in ms */

View file

@ -14,7 +14,7 @@ import { logger } from '@librechat/data-schemas';
import session, { MemoryStore } from 'express-session';
import { RedisStore as ConnectRedis } from 'connect-redis';
import type { SendCommandFn } from 'rate-limit-redis';
import { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } from './redisClients';
import { keyvRedisClient, ioredisClient } from './redisClients';
import { cacheConfig } from './cacheConfig';
import { violationFile } from './keyvFiles';
@ -31,7 +31,7 @@ export const standardCache = (namespace: string, ttl?: number, fallbackStore?: o
const keyvRedis = new KeyvRedis(keyvRedisClient);
const cache = new Keyv(keyvRedis, { namespace, ttl });
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;
keyvRedis.keyPrefixSeparator = GLOBAL_PREFIX_SEPARATOR;
keyvRedis.keyPrefixSeparator = cacheConfig.GLOBAL_PREFIX_SEPARATOR;
cache.on('error', (err) => {
logger.error(`Cache error in namespace ${namespace}:`, err);

View file

@ -5,8 +5,6 @@ import { createClient, createCluster } from '@keyv/redis';
import type { RedisClientType, RedisClusterType } from '@redis/client';
import { cacheConfig } from './cacheConfig';
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;
@ -18,7 +16,7 @@ if (cacheConfig.USE_REDIS) {
username: username,
password: password,
tls: ca ? { ca } : undefined,
keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${GLOBAL_PREFIX_SEPARATOR}`,
keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${cacheConfig.GLOBAL_PREFIX_SEPARATOR}`,
maxListeners: cacheConfig.REDIS_MAX_LISTENERS,
retryStrategy: (times: number) => {
if (
@ -192,4 +190,4 @@ if (cacheConfig.USE_REDIS) {
});
}
export { ioredisClient, keyvRedisClient, GLOBAL_PREFIX_SEPARATOR };
export { ioredisClient, keyvRedisClient };