From 8c531b921e8ef93d652dad9a1d0c39ee5e17a2a6 Mon Sep 17 00:00:00 2001 From: "Theo N. Truong" <644650+nhtruong@users.noreply.github.com> Date: Sun, 16 Nov 2025 09:58:52 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Redis=20Cluster=20Bug=20+?= =?UTF-8?q?=20=F0=9F=A7=AA=20Enhance=20Test=20Coverage=20(#10518)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ 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 --- .github/workflows/cache-integration-tests.yml | 21 ++++------ packages/api/package.json | 1 + .../limiterCache.cache_integration.spec.ts | 16 ++------ .../sessionCache.cache_integration.spec.ts | 28 ++----------- .../standardCache.cache_integration.spec.ts | 24 ++---------- .../violationCache.cache_integration.spec.ts | 28 ++----------- .../redisClients.cache_integration.spec.ts | 39 +++++++------------ packages/api/src/cache/redisClients.ts | 29 +++++++++++++- .../LeaderElection.cache_integration.spec.ts | 6 +-- ...rversInitializer.cache_integration.spec.ts | 4 +- ...PServersRegistry.cache_integration.spec.ts | 4 +- .../registry/cache/ServerConfigsCacheRedis.ts | 2 + ...istryStatusCache.cache_integration.spec.ts | 4 +- ...onfigsCacheRedis.cache_integration.spec.ts | 9 +++-- 14 files changed, 81 insertions(+), 134 deletions(-) diff --git a/.github/workflows/cache-integration-tests.yml b/.github/workflows/cache-integration-tests.yml index bdd3f2e83d..1f056dd791 100644 --- a/.github/workflows/cache-integration-tests.yml +++ b/.github/workflows/cache-integration-tests.yml @@ -61,30 +61,23 @@ jobs: npm run build:data-schemas npm run build:api - - name: Run cache integration tests + - name: Run all cache integration tests (Single Redis Node) working-directory: packages/api env: NODE_ENV: test USE_REDIS: true + USE_REDIS_CLUSTER: false 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:core + run: npm run test:cache-integration - - name: Run cluster integration tests + - name: Run all cache integration tests (Redis Cluster) 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:cluster - - - 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 + USE_REDIS_CLUSTER: true + 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: Stop Redis Cluster if: always() diff --git a/packages/api/package.json b/packages/api/package.json index e3b9a13b38..44fe3963ac 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,6 +23,7 @@ "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: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", "b:clean": "bun run rimraf dist", "b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs", diff --git a/packages/api/src/cache/__tests__/cacheFactory/limiterCache.cache_integration.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/limiterCache.cache_integration.spec.ts index ab30ec3d1f..52cfee1f07 100644 --- a/packages/api/src/cache/__tests__/cacheFactory/limiterCache.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/cacheFactory/limiterCache.cache_integration.spec.ts @@ -7,17 +7,13 @@ describe('limiterCache', () => { beforeEach(() => { originalEnv = { ...process.env }; - // Clear cache-related env vars - 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 + // Set test configuration with fallback defaults for local testing process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; 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 jest.resetModules(); @@ -43,10 +39,6 @@ describe('limiterCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; diff --git a/packages/api/src/cache/__tests__/cacheFactory/sessionCache.cache_integration.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/sessionCache.cache_integration.spec.ts index 144eee557d..f4ded8bc74 100644 --- a/packages/api/src/cache/__tests__/cacheFactory/sessionCache.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/cacheFactory/sessionCache.cache_integration.spec.ts @@ -33,17 +33,13 @@ describe('sessionCache', () => { beforeEach(() => { originalEnv = { ...process.env }; - // Clear cache-related env vars - 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 + // Set test configuration with fallback defaults for local testing process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; 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 jest.resetModules(); @@ -55,10 +51,6 @@ describe('sessionCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; @@ -138,10 +130,6 @@ describe('sessionCache', () => { }); 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 store1 = cacheFactory.sessionCache('namespace1'); @@ -152,10 +140,6 @@ describe('sessionCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; @@ -173,10 +157,6 @@ describe('sessionCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; diff --git a/packages/api/src/cache/__tests__/cacheFactory/standardCache.cache_integration.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/standardCache.cache_integration.spec.ts index b5fcc207da..6351317084 100644 --- a/packages/api/src/cache/__tests__/cacheFactory/standardCache.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/cacheFactory/standardCache.cache_integration.spec.ts @@ -30,18 +30,13 @@ describe('standardCache', () => { beforeEach(() => { originalEnv = { ...process.env }; - // Clear cache-related env vars - 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 + // Set test configuration with fallback defaults for local testing process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; 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 jest.resetModules(); @@ -119,10 +114,6 @@ describe('standardCache', () => { describe('when connecting to a Redis server', () => { 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 cache1 = cacheFactory.standardCache('namespace-one'); @@ -148,9 +139,6 @@ describe('standardCache', () => { }); 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 const cacheFactory = await import('../../cacheFactory'); @@ -167,10 +155,6 @@ describe('standardCache', () => { }); 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'); testCache = cacheFactory.standardCache('ttl-test', 1000); // 1 second TTL diff --git a/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts b/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts index 7ffec8d74e..989008e82e 100644 --- a/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/cacheFactory/violationCache.cache_integration.spec.ts @@ -26,17 +26,13 @@ describe('violationCache', () => { beforeEach(() => { originalEnv = { ...process.env }; - // Clear cache-related env vars - 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 + // Set test configuration with fallback defaults for local testing process.env.REDIS_PING_INTERVAL = '0'; process.env.REDIS_KEY_PREFIX = 'Cache-Integration-Test'; 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 jest.resetModules(); @@ -48,10 +44,6 @@ describe('violationCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; @@ -119,10 +111,6 @@ describe('violationCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; @@ -157,10 +145,6 @@ describe('violationCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; @@ -193,10 +177,6 @@ describe('violationCache', () => { }); 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 redisClients = await import('../../redisClients'); const { ioredisClient } = redisClients; diff --git a/packages/api/src/cache/__tests__/redisClients.cache_integration.spec.ts b/packages/api/src/cache/__tests__/redisClients.cache_integration.spec.ts index e2bbda7224..dc9a325746 100644 --- a/packages/api/src/cache/__tests__/redisClients.cache_integration.spec.ts +++ b/packages/api/src/cache/__tests__/redisClients.cache_integration.spec.ts @@ -9,9 +9,13 @@ describe('redisClients Integration Tests', () => { let keyvRedisClient: RedisClientType | RedisClusterType | null = null; // Helper function to test set/get/delete operations - const testRedisOperations = async (client: RedisClient, keyPrefix: string): Promise => { - // Wait cluster to fully initialize - await new Promise((resolve) => setTimeout(resolve, 1000)); + const testRedisOperations = async ( + client: RedisClient, + keyPrefix: string, + readyPromise?: Promise, + ): Promise => { + // Wait for connection and topology discovery to complete + if (readyPromise) await readyPromise; const testKey = `${keyPrefix}-test-key`; const testValue = `${keyPrefix}-test-value`; @@ -35,18 +39,13 @@ describe('redisClients Integration Tests', () => { beforeEach(() => { originalEnv = { ...process.env }; - // Clear Redis-related env vars - 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 common test configuration - process.env.REDIS_PING_INTERVAL = '0'; + // Set common test configuration with fallback defaults for local testing + process.env.REDIS_PING_INTERVAL = '1000'; process.env.REDIS_KEY_PREFIX = 'Redis-Integration-Test'; 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 jest.resetModules(); @@ -105,10 +104,6 @@ describe('redisClients Integration Tests', () => { describe('when connecting to a Redis instance', () => { 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'); ioredisClient = clients.ioredisClient; await testRedisOperations(ioredisClient!, 'ioredis-single'); @@ -117,7 +112,6 @@ describe('redisClients Integration Tests', () => { describe('when connecting to a Redis cluster', () => { 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.REDIS_URI = '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', () => { 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'); keyvRedisClient = clients.keyvRedisClient; - await testRedisOperations(keyvRedisClient!, 'keyv-single'); + await testRedisOperations(keyvRedisClient!, 'keyv-single', clients.keyvRedisClientReady!); }); }); describe('when connecting to a Redis cluster', () => { 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.REDIS_URI = 'redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003'; const clients = await import('../redisClients'); keyvRedisClient = clients.keyvRedisClient; - await testRedisOperations(keyvRedisClient!, 'keyv-cluster'); + await testRedisOperations(keyvRedisClient!, 'keyv-cluster', clients.keyvRedisClientReady!); }); }); }); diff --git a/packages/api/src/cache/redisClients.ts b/packages/api/src/cache/redisClients.ts index 6f0e27d772..79489336c4 100644 --- a/packages/api/src/cache/redisClients.ts +++ b/packages/api/src/cache/redisClients.ts @@ -3,6 +3,7 @@ import type { Redis, Cluster } from 'ioredis'; import { logger } from '@librechat/data-schemas'; import { createClient, createCluster } from '@keyv/redis'; import type { RedisClientType, RedisClusterType } from '@redis/client'; +import type { ScanCommandOptions } from '@redis/client/dist/lib/commands/SCAN'; import { cacheConfig } from './cacheConfig'; 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 keyvRedisClientReady: + | Promise + | Promise, Record, Record>> + | null = null; + if (cacheConfig.USE_REDIS) { /** * ** WARNING ** Keyv Redis client does not support Prefix like ioredis above. @@ -162,6 +168,22 @@ if (cacheConfig.USE_REDIS) { 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.on('error', (err) => { @@ -184,10 +206,13 @@ if (cacheConfig.USE_REDIS) { 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); throw err; }); } -export { ioredisClient, keyvRedisClient }; +export { ioredisClient, keyvRedisClient, keyvRedisClientReady }; diff --git a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts index 60bc1b439b..b37c291880 100644 --- a/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts +++ b/packages/api/src/cluster/__tests__/LeaderElection.cache_integration.spec.ts @@ -25,10 +25,8 @@ describe('LeaderElection with Redis', () => { throw new Error('Redis client is not initialized'); } - // Wait for Redis to be ready - if (!keyvRedisClient.isOpen) { - await keyvRedisClient.connect(); - } + // Wait for connection and topology discovery to complete + await redisClients.keyvRedisClientReady; // Increase max listeners to handle many instances in tests process.setMaxListeners(200); diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts index 820cdfa54e..623f5081fc 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersInitializer.cache_integration.spec.ts @@ -121,8 +121,8 @@ describe('MCPServersInitializer Redis Integration Tests', () => { // Ensure Redis is connected if (!keyvRedisClient) throw new Error('Redis client is not initialized'); - // Wait for Redis to be ready - if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); + // Wait for connection and topology discovery to complete + await redisClients.keyvRedisClientReady; // Become leader so we can perform write operations leaderInstance = new LeaderElection(); diff --git a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts index 68e9291d46..13f97ee20e 100644 --- a/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/__tests__/MCPServersRegistry.cache_integration.spec.ts @@ -50,8 +50,8 @@ describe('MCPServersRegistry Redis Integration Tests', () => { // Ensure Redis is connected if (!keyvRedisClient) throw new Error('Redis client is not initialized'); - // Wait for Redis to be ready - if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); + // Wait for connection and topology discovery to complete + await redisClients.keyvRedisClientReady; // Become leader so we can perform write operations leaderInstance = new LeaderElection(); diff --git a/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts b/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts index a2e025736c..da582e5f29 100644 --- a/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts +++ b/packages/api/src/mcp/registry/cache/ServerConfigsCacheRedis.ts @@ -73,6 +73,8 @@ export class ServerConfigsCacheRedis extends BaseRegistryCache { entries.push([keyName, value as ParsedServerConfig]); } } + } else { + throw new Error('Redis client with scanIterator not available.'); } return fromPairs(entries); diff --git a/packages/api/src/mcp/registry/cache/__tests__/RegistryStatusCache.cache_integration.spec.ts b/packages/api/src/mcp/registry/cache/__tests__/RegistryStatusCache.cache_integration.spec.ts index 643e7c27df..41e7781299 100644 --- a/packages/api/src/mcp/registry/cache/__tests__/RegistryStatusCache.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/cache/__tests__/RegistryStatusCache.cache_integration.spec.ts @@ -25,8 +25,8 @@ describe('RegistryStatusCache Integration Tests', () => { // Ensure Redis is connected if (!keyvRedisClient) throw new Error('Redis client is not initialized'); - // Wait for Redis to be ready - if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); + // Wait for connection and topology discovery to complete + await redisClients.keyvRedisClientReady; // Become leader so we can perform write operations leaderInstance = new LeaderElection(); diff --git a/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts b/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts index 7e139dc5be..924f3f8061 100644 --- a/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts +++ b/packages/api/src/mcp/registry/cache/__tests__/ServerConfigsCacheRedis.cache_integration.spec.ts @@ -31,7 +31,10 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { beforeAll(async () => { // Set up environment variables for Redis (only if not already set) 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 ?? 'ServerConfigsCacheRedis-IntegrationTest'; @@ -49,8 +52,8 @@ describe('ServerConfigsCacheRedis Integration Tests', () => { // Ensure Redis is connected if (!keyvRedisClient) throw new Error('Redis client is not initialized'); - // Wait for Redis to be ready - if (!keyvRedisClient.isOpen) await keyvRedisClient.connect(); + // Wait for connection and topology discovery to complete + await redisClients.keyvRedisClientReady; // Clear any existing leader key to ensure clean state await keyvRedisClient.del(LeaderElection.LEADER_KEY);