mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🐛 fix: Redis Cluster Bug + 🧪 Enhance Test Coverage (#10518)
* ✨ feat: Implement scanIterator method for Redis cluster client This resolves the bug where `ServerConfigsCacheRedis#getAll` returns an empty object when a Redis Cluster (instead of a single node server is used) * ✨ feat: Update cache integration tests for Redis cluster support
This commit is contained in:
parent
f228f2a91d
commit
8c531b921e
14 changed files with 81 additions and 134 deletions
21
.github/workflows/cache-integration-tests.yml
vendored
21
.github/workflows/cache-integration-tests.yml
vendored
|
|
@ -61,30 +61,23 @@ jobs:
|
||||||
npm run build:data-schemas
|
npm run build:data-schemas
|
||||||
npm run build:api
|
npm run build:api
|
||||||
|
|
||||||
- name: Run cache integration tests
|
- name: Run all cache integration tests (Single Redis Node)
|
||||||
working-directory: packages/api
|
working-directory: packages/api
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
USE_REDIS: true
|
USE_REDIS: true
|
||||||
|
USE_REDIS_CLUSTER: false
|
||||||
REDIS_URI: redis://127.0.0.1:6379
|
REDIS_URI: redis://127.0.0.1:6379
|
||||||
REDIS_CLUSTER_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
run: npm run test:cache-integration
|
||||||
run: npm run test:cache-integration:core
|
|
||||||
|
|
||||||
- name: Run cluster integration tests
|
- name: Run all cache integration tests (Redis Cluster)
|
||||||
working-directory: packages/api
|
working-directory: packages/api
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
USE_REDIS: true
|
USE_REDIS: true
|
||||||
REDIS_URI: redis://127.0.0.1:6379
|
USE_REDIS_CLUSTER: true
|
||||||
run: npm run test:cache-integration:cluster
|
REDIS_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
||||||
|
run: npm run test:cache-integration
|
||||||
- name: Run mcp integration tests
|
|
||||||
working-directory: packages/api
|
|
||||||
env:
|
|
||||||
NODE_ENV: test
|
|
||||||
USE_REDIS: true
|
|
||||||
REDIS_URI: redis://127.0.0.1:6379
|
|
||||||
run: npm run test:cache-integration:mcp
|
|
||||||
|
|
||||||
- name: Stop Redis Cluster
|
- name: Stop Redis Cluster
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"test:cache-integration:core": "jest --testPathPattern=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
"test:cache-integration:core": "jest --testPathPattern=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
||||||
"test:cache-integration:cluster": "jest --testPathPattern=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand",
|
"test:cache-integration:cluster": "jest --testPathPattern=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand",
|
||||||
"test:cache-integration:mcp": "jest --testPathPattern=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
"test:cache-integration:mcp": "jest --testPathPattern=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
||||||
|
"test:cache-integration": "npm run test:cache-integration:core && npm run test:cache-integration:cluster && npm run test:cache-integration:mcp",
|
||||||
"verify": "npm run test:ci",
|
"verify": "npm run test:ci",
|
||||||
"b:clean": "bun run rimraf dist",
|
"b:clean": "bun run rimraf dist",
|
||||||
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs",
|
"b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs",
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,13 @@ describe('limiterCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Clear cache-related env vars
|
// Set test configuration with fallback defaults for local testing
|
||||||
delete process.env.USE_REDIS;
|
|
||||||
delete process.env.REDIS_URI;
|
|
||||||
delete process.env.USE_REDIS_CLUSTER;
|
|
||||||
delete process.env.REDIS_PING_INTERVAL;
|
|
||||||
delete process.env.REDIS_KEY_PREFIX;
|
|
||||||
|
|
||||||
// Set test configuration
|
|
||||||
process.env.REDIS_PING_INTERVAL = '0';
|
process.env.REDIS_PING_INTERVAL = '0';
|
||||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||||
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
||||||
|
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
|
||||||
|
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
|
||||||
|
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
|
||||||
|
|
||||||
// Clear require cache to reload modules
|
// Clear require cache to reload modules
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -43,10 +39,6 @@ describe('limiterCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return RedisStore with sendCommand when USE_REDIS is true', async () => {
|
test('should return RedisStore with sendCommand when USE_REDIS is true', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,13 @@ describe('sessionCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Clear cache-related env vars
|
// Set test configuration with fallback defaults for local testing
|
||||||
delete process.env.USE_REDIS;
|
|
||||||
delete process.env.REDIS_URI;
|
|
||||||
delete process.env.USE_REDIS_CLUSTER;
|
|
||||||
delete process.env.REDIS_PING_INTERVAL;
|
|
||||||
delete process.env.REDIS_KEY_PREFIX;
|
|
||||||
|
|
||||||
// Set test configuration
|
|
||||||
process.env.REDIS_PING_INTERVAL = '0';
|
process.env.REDIS_PING_INTERVAL = '0';
|
||||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||||
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
||||||
|
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
|
||||||
|
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
|
||||||
|
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
|
||||||
|
|
||||||
// Clear require cache to reload modules
|
// Clear require cache to reload modules
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -55,10 +51,6 @@ describe('sessionCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return ConnectRedis store when USE_REDIS is true', async () => {
|
test('should return ConnectRedis store when USE_REDIS is true', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
@ -138,10 +130,6 @@ describe('sessionCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle namespace with and without trailing colon', async () => {
|
test('should handle namespace with and without trailing colon', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
|
|
||||||
const store1 = cacheFactory.sessionCache('namespace1');
|
const store1 = cacheFactory.sessionCache('namespace1');
|
||||||
|
|
@ -152,10 +140,6 @@ describe('sessionCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should register error handler for Redis connection', async () => {
|
test('should register error handler for Redis connection', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
@ -173,10 +157,6 @@ describe('sessionCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle session expiration with TTL', async () => {
|
test('should handle session expiration with TTL', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
|
||||||
|
|
@ -30,18 +30,13 @@ describe('standardCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Clear cache-related env vars
|
// Set test configuration with fallback defaults for local testing
|
||||||
delete process.env.USE_REDIS;
|
|
||||||
delete process.env.REDIS_URI;
|
|
||||||
delete process.env.USE_REDIS_CLUSTER;
|
|
||||||
delete process.env.REDIS_PING_INTERVAL;
|
|
||||||
delete process.env.REDIS_KEY_PREFIX;
|
|
||||||
delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES;
|
|
||||||
|
|
||||||
// Set test configuration
|
|
||||||
process.env.REDIS_PING_INTERVAL = '0';
|
process.env.REDIS_PING_INTERVAL = '0';
|
||||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||||
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
||||||
|
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
|
||||||
|
process.env.USE_REDIS_CLUSTER = 'false';
|
||||||
|
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
||||||
|
|
||||||
// Clear require cache to reload modules
|
// Clear require cache to reload modules
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -119,10 +114,6 @@ describe('standardCache', () => {
|
||||||
|
|
||||||
describe('when connecting to a Redis server', () => {
|
describe('when connecting to a Redis server', () => {
|
||||||
test('should handle different namespaces with correct prefixes', async () => {
|
test('should handle different namespaces with correct prefixes', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
|
|
||||||
const cache1 = cacheFactory.standardCache('namespace-one');
|
const cache1 = cacheFactory.standardCache('namespace-one');
|
||||||
|
|
@ -148,9 +139,6 @@ describe('standardCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect FORCED_IN_MEMORY_CACHE_NAMESPACES', async () => {
|
test('should respect FORCED_IN_MEMORY_CACHE_NAMESPACES', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'ROLES'; // Use a valid cache key
|
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'ROLES'; // Use a valid cache key
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
|
|
@ -167,10 +155,6 @@ describe('standardCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle TTL correctly', async () => {
|
test('should handle TTL correctly', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL
|
testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,13 @@ describe('violationCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Clear cache-related env vars
|
// Set test configuration with fallback defaults for local testing
|
||||||
delete process.env.USE_REDIS;
|
|
||||||
delete process.env.REDIS_URI;
|
|
||||||
delete process.env.USE_REDIS_CLUSTER;
|
|
||||||
delete process.env.REDIS_PING_INTERVAL;
|
|
||||||
delete process.env.REDIS_KEY_PREFIX;
|
|
||||||
|
|
||||||
// Set test configuration
|
|
||||||
process.env.REDIS_PING_INTERVAL = '0';
|
process.env.REDIS_PING_INTERVAL = '0';
|
||||||
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test';
|
||||||
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
||||||
|
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
|
||||||
|
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
|
||||||
|
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
|
||||||
|
|
||||||
// Clear require cache to reload modules
|
// Clear require cache to reload modules
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -48,10 +44,6 @@ describe('violationCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create violation cache with Redis when USE_REDIS is true', async () => {
|
test('should create violation cache with Redis when USE_REDIS is true', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
@ -119,10 +111,6 @@ describe('violationCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect namespace prefixing', async () => {
|
test('should respect namespace prefixing', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
@ -157,10 +145,6 @@ describe('violationCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect TTL settings', async () => {
|
test('should respect TTL settings', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
@ -193,10 +177,6 @@ describe('violationCache', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle complex violation data structures', async () => {
|
test('should handle complex violation data structures', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const cacheFactory = await import('../../cacheFactory');
|
const cacheFactory = await import('../../cacheFactory');
|
||||||
const redisClients = await import('../../redisClients');
|
const redisClients = await import('../../redisClients');
|
||||||
const { ioredisClient } = redisClients;
|
const { ioredisClient } = redisClients;
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ describe('redisClients Integration Tests', () => {
|
||||||
let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
|
let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
|
||||||
|
|
||||||
// Helper function to test set/get/delete operations
|
// Helper function to test set/get/delete operations
|
||||||
const testRedisOperations = async (client: RedisClient, keyPrefix: string): Promise<void> => {
|
const testRedisOperations = async (
|
||||||
// Wait cluster to fully initialize
|
client: RedisClient,
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
keyPrefix: string,
|
||||||
|
readyPromise?: Promise<void>,
|
||||||
|
): Promise<void> => {
|
||||||
|
// Wait for connection and topology discovery to complete
|
||||||
|
if (readyPromise) await readyPromise;
|
||||||
|
|
||||||
const testKey = `${keyPrefix}-test-key`;
|
const testKey = `${keyPrefix}-test-key`;
|
||||||
const testValue = `${keyPrefix}-test-value`;
|
const testValue = `${keyPrefix}-test-value`;
|
||||||
|
|
@ -35,18 +39,13 @@ describe('redisClients Integration Tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Clear Redis-related env vars
|
// Set common test configuration with fallback defaults for local testing
|
||||||
delete process.env.USE_REDIS;
|
process.env.REDIS_PING_INTERVAL = '1000';
|
||||||
delete process.env.REDIS_URI;
|
|
||||||
delete process.env.USE_REDIS_CLUSTER;
|
|
||||||
delete process.env.REDIS_PING_INTERVAL;
|
|
||||||
delete process.env.REDIS_KEY_PREFIX;
|
|
||||||
|
|
||||||
// Set common test configuration
|
|
||||||
process.env.REDIS_PING_INTERVAL = '0';
|
|
||||||
process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test';
|
process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test';
|
||||||
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
process.env.REDIS_RETRY_MAX_ATTEMPTS = '5';
|
||||||
process.env.REDIS_PING_INTERVAL = '1000';
|
process.env.USE_REDIS = process.env.USE_REDIS || 'true';
|
||||||
|
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER || 'false';
|
||||||
|
process.env.REDIS_URI = process.env.REDIS_URI || 'redis://127.0.0.1:6379';
|
||||||
|
|
||||||
// Clear module cache to reload module
|
// Clear module cache to reload module
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -105,10 +104,6 @@ describe('redisClients Integration Tests', () => {
|
||||||
|
|
||||||
describe('when connecting to a Redis instance', () => {
|
describe('when connecting to a Redis instance', () => {
|
||||||
test('should connect and perform set/get/delete operations', async () => {
|
test('should connect and perform set/get/delete operations', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const clients = await import('../redisClients');
|
const clients = await import('../redisClients');
|
||||||
ioredisClient = clients.ioredisClient;
|
ioredisClient = clients.ioredisClient;
|
||||||
await testRedisOperations(ioredisClient!, 'ioredis-single');
|
await testRedisOperations(ioredisClient!, 'ioredis-single');
|
||||||
|
|
@ -117,7 +112,6 @@ describe('redisClients Integration Tests', () => {
|
||||||
|
|
||||||
describe('when connecting to a Redis cluster', () => {
|
describe('when connecting to a Redis cluster', () => {
|
||||||
test('should connect to cluster and perform set/get/delete operations', async () => {
|
test('should connect to cluster and perform set/get/delete operations', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'true';
|
process.env.USE_REDIS_CLUSTER = 'true';
|
||||||
process.env.REDIS_URI =
|
process.env.REDIS_URI =
|
||||||
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
||||||
|
|
@ -142,26 +136,21 @@ describe('redisClients Integration Tests', () => {
|
||||||
|
|
||||||
describe('when connecting to a Redis instance', () => {
|
describe('when connecting to a Redis instance', () => {
|
||||||
test('should connect and perform set/get/delete operations', async () => {
|
test('should connect and perform set/get/delete operations', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'false';
|
|
||||||
process.env.REDIS_URI = 'redis://127.0.0.1:6379';
|
|
||||||
|
|
||||||
const clients = await import('../redisClients');
|
const clients = await import('../redisClients');
|
||||||
keyvRedisClient = clients.keyvRedisClient;
|
keyvRedisClient = clients.keyvRedisClient;
|
||||||
await testRedisOperations(keyvRedisClient!, 'keyv-single');
|
await testRedisOperations(keyvRedisClient!, 'keyv-single', clients.keyvRedisClientReady!);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when connecting to a Redis cluster', () => {
|
describe('when connecting to a Redis cluster', () => {
|
||||||
test('should connect to cluster and perform set/get/delete operations', async () => {
|
test('should connect to cluster and perform set/get/delete operations', async () => {
|
||||||
process.env.USE_REDIS = 'true';
|
|
||||||
process.env.USE_REDIS_CLUSTER = 'true';
|
process.env.USE_REDIS_CLUSTER = 'true';
|
||||||
process.env.REDIS_URI =
|
process.env.REDIS_URI =
|
||||||
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
||||||
|
|
||||||
const clients = await import('../redisClients');
|
const clients = await import('../redisClients');
|
||||||
keyvRedisClient = clients.keyvRedisClient;
|
keyvRedisClient = clients.keyvRedisClient;
|
||||||
await testRedisOperations(keyvRedisClient!, 'keyv-cluster');
|
await testRedisOperations(keyvRedisClient!, 'keyv-cluster', clients.keyvRedisClientReady!);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
29
packages/api/src/cache/redisClients.ts
vendored
29
packages/api/src/cache/redisClients.ts
vendored
|
|
@ -3,6 +3,7 @@ import type { Redis, Cluster } from 'ioredis';
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { createClient, createCluster } from '@keyv/redis';
|
import { createClient, createCluster } from '@keyv/redis';
|
||||||
import type { RedisClientType, RedisClusterType } from '@redis/client';
|
import type { RedisClientType, RedisClusterType } from '@redis/client';
|
||||||
|
import type { ScanCommandOptions } from '@redis/client/dist/lib/commands/SCAN';
|
||||||
import { cacheConfig } from './cacheConfig';
|
import { cacheConfig } from './cacheConfig';
|
||||||
|
|
||||||
const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri)) || [];
|
const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri)) || [];
|
||||||
|
|
@ -121,6 +122,11 @@ if (cacheConfig.USE_REDIS) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
|
let keyvRedisClient: RedisClientType | RedisClusterType | null = null;
|
||||||
|
let keyvRedisClientReady:
|
||||||
|
| Promise<void>
|
||||||
|
| Promise<RedisClientType<Record<string, never>, Record<string, never>, Record<string, never>>>
|
||||||
|
| null = null;
|
||||||
|
|
||||||
if (cacheConfig.USE_REDIS) {
|
if (cacheConfig.USE_REDIS) {
|
||||||
/**
|
/**
|
||||||
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
|
* ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
|
||||||
|
|
@ -162,6 +168,22 @@ if (cacheConfig.USE_REDIS) {
|
||||||
defaults: redisOptions,
|
defaults: redisOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add scanIterator method to cluster client for API consistency with standalone client
|
||||||
|
if (!('scanIterator' in keyvRedisClient)) {
|
||||||
|
const clusterClient = keyvRedisClient as RedisClusterType;
|
||||||
|
(keyvRedisClient as unknown as RedisClientType).scanIterator = async function* (
|
||||||
|
options?: ScanCommandOptions,
|
||||||
|
) {
|
||||||
|
const masters = clusterClient.masters;
|
||||||
|
for (const master of masters) {
|
||||||
|
const nodeClient = await clusterClient.nodeClient(master);
|
||||||
|
for await (const key of nodeClient.scanIterator(options)) {
|
||||||
|
yield key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
|
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
|
||||||
|
|
||||||
keyvRedisClient.on('error', (err) => {
|
keyvRedisClient.on('error', (err) => {
|
||||||
|
|
@ -184,10 +206,13 @@ if (cacheConfig.USE_REDIS) {
|
||||||
logger.warn('@keyv/redis client disconnected');
|
logger.warn('@keyv/redis client disconnected');
|
||||||
});
|
});
|
||||||
|
|
||||||
keyvRedisClient.connect().catch((err) => {
|
// Start connection immediately
|
||||||
|
keyvRedisClientReady = keyvRedisClient.connect();
|
||||||
|
|
||||||
|
keyvRedisClientReady.catch((err): void => {
|
||||||
logger.error('@keyv/redis initial connection failed:', err);
|
logger.error('@keyv/redis initial connection failed:', err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ioredisClient, keyvRedisClient };
|
export { ioredisClient, keyvRedisClient, keyvRedisClientReady };
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,8 @@ describe('LeaderElection with Redis', () => {
|
||||||
throw new Error('Redis client is not initialized');
|
throw new Error('Redis client is not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for Redis to be ready
|
// Wait for connection and topology discovery to complete
|
||||||
if (!keyvRedisClient.isOpen) {
|
await redisClients.keyvRedisClientReady;
|
||||||
await keyvRedisClient.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increase max listeners to handle many instances in tests
|
// Increase max listeners to handle many instances in tests
|
||||||
process.setMaxListeners(200);
|
process.setMaxListeners(200);
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,8 @@ describe('MCPServersInitializer Redis Integration Tests', () => {
|
||||||
// Ensure Redis is connected
|
// Ensure Redis is connected
|
||||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||||
|
|
||||||
// Wait for Redis to be ready
|
// Wait for connection and topology discovery to complete
|
||||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
await redisClients.keyvRedisClientReady;
|
||||||
|
|
||||||
// Become leader so we can perform write operations
|
// Become leader so we can perform write operations
|
||||||
leaderInstance = new LeaderElection();
|
leaderInstance = new LeaderElection();
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ describe('MCPServersRegistry Redis Integration Tests', () => {
|
||||||
// Ensure Redis is connected
|
// Ensure Redis is connected
|
||||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||||
|
|
||||||
// Wait for Redis to be ready
|
// Wait for connection and topology discovery to complete
|
||||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
await redisClients.keyvRedisClientReady;
|
||||||
|
|
||||||
// Become leader so we can perform write operations
|
// Become leader so we can perform write operations
|
||||||
leaderInstance = new LeaderElection();
|
leaderInstance = new LeaderElection();
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ export class ServerConfigsCacheRedis extends BaseRegistryCache {
|
||||||
entries.push([keyName, value as ParsedServerConfig]);
|
entries.push([keyName, value as ParsedServerConfig]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Redis client with scanIterator not available.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromPairs(entries);
|
return fromPairs(entries);
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ describe('RegistryStatusCache Integration Tests', () => {
|
||||||
// Ensure Redis is connected
|
// Ensure Redis is connected
|
||||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||||
|
|
||||||
// Wait for Redis to be ready
|
// Wait for connection and topology discovery to complete
|
||||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
await redisClients.keyvRedisClientReady;
|
||||||
|
|
||||||
// Become leader so we can perform write operations
|
// Become leader so we can perform write operations
|
||||||
leaderInstance = new LeaderElection();
|
leaderInstance = new LeaderElection();
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Set up environment variables for Redis (only if not already set)
|
// Set up environment variables for Redis (only if not already set)
|
||||||
process.env.USE_REDIS = process.env.USE_REDIS ?? 'true';
|
process.env.USE_REDIS = process.env.USE_REDIS ?? 'true';
|
||||||
process.env.REDIS_URI = process.env.REDIS_URI ?? 'redis://127.0.0.1:6379';
|
process.env.USE_REDIS_CLUSTER = process.env.USE_REDIS_CLUSTER ?? 'true';
|
||||||
|
process.env.REDIS_URI =
|
||||||
|
process.env.REDIS_URI ??
|
||||||
|
'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003';
|
||||||
process.env.REDIS_KEY_PREFIX =
|
process.env.REDIS_KEY_PREFIX =
|
||||||
process.env.REDIS_KEY_PREFIX ?? 'ServerConfigsCacheRedis-IntegrationTest';
|
process.env.REDIS_KEY_PREFIX ?? 'ServerConfigsCacheRedis-IntegrationTest';
|
||||||
|
|
||||||
|
|
@ -49,8 +52,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => {
|
||||||
// Ensure Redis is connected
|
// Ensure Redis is connected
|
||||||
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
if (!keyvRedisClient) throw new Error('Redis client is not initialized');
|
||||||
|
|
||||||
// Wait for Redis to be ready
|
// Wait for connection and topology discovery to complete
|
||||||
if (!keyvRedisClient.isOpen) await keyvRedisClient.connect();
|
await redisClients.keyvRedisClientReady;
|
||||||
|
|
||||||
// Clear any existing leader key to ensure clean state
|
// Clear any existing leader key to ensure clean state
|
||||||
await keyvRedisClient.del(LeaderElection.LEADER_KEY);
|
await keyvRedisClient.del(LeaderElection.LEADER_KEY);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue