From dcad5c2b90b10376c15ed62ecbd62df544b2b237 Mon Sep 17 00:00:00 2001 From: Matsu Date: Tue, 9 Jun 2026 12:32:20 +0300 Subject: [PATCH] test(core): Stop Redis retryStrategy from exiting the process under test (#31959) Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/redis-client.service.test.ts | 34 +++++++++++++++++++ .../cli/src/services/redis-client.service.ts | 15 ++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/services/__tests__/redis-client.service.test.ts b/packages/cli/src/services/__tests__/redis-client.service.test.ts index a4fc458fe1b..986e5496ccb 100644 --- a/packages/cli/src/services/__tests__/redis-client.service.test.ts +++ b/packages/cli/src/services/__tests__/redis-client.service.test.ts @@ -7,6 +7,14 @@ import { RedisClientService } from '@/services/redis-client.service'; type EventHandler = (...args: unknown[]) => void; +/** + * The service disables process exit under test by default. Re-enable it so the + * cumulative-timeout accounting tests can assert on the exit decision. + */ +const enableExitOnRedisUnreachable = (service: RedisClientService) => { + (service as unknown as { exitOnRedisUnreachable: boolean }).exitOnRedisUnreachable = true; +}; + jest.mock('ioredis', () => { return jest.fn().mockImplementation(() => { return { @@ -129,6 +137,7 @@ describe('RedisClientService', () => { const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); const service = new RedisClientService(logger, globalConfig); + enableExitOnRedisUnreachable(service); service.createClient({ type: 'client(bull)' }); service.createClient({ type: 'subscriber(bull)' }); @@ -168,6 +177,7 @@ describe('RedisClientService', () => { const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); const service = new RedisClientService(logger, globalConfig); + enableExitOnRedisUnreachable(service); service.createClient({ type: 'client(bull)' }); service.createClient({ type: 'subscriber(bull)' }); @@ -219,6 +229,7 @@ describe('RedisClientService', () => { const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); const service = new RedisClientService(logger, globalConfig); + enableExitOnRedisUnreachable(service); service.createClient({ type: 'client(bull)' }); const mockClient = mockedRedis.mock.results[0].value; @@ -247,5 +258,28 @@ describe('RedisClientService', () => { dateNowSpy.mockRestore(); exitSpy.mockRestore(); }); + + it('should not exit the process under test even when the timeout is exceeded', () => { + const T0 = 1_700_000_000_000; + const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(T0); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + // No enableExitOnRedisUnreachable() — exercise the default test behaviour. + const service = new RedisClientService(logger, globalConfig); + service.createClient({ type: 'client(bull)' }); + + const retryStrategy = mockedRedis.mock.calls[0][0].retryStrategy as () => number; + + // Fail continuously well past the timeout threshold. + dateNowSpy.mockReturnValue(T0 + 1_000); + retryStrategy(); + dateNowSpy.mockReturnValue(T0 + 12_001); + retryStrategy(); + + expect(exitSpy).not.toHaveBeenCalled(); + + dateNowSpy.mockRestore(); + exitSpy.mockRestore(); + }); }); }); diff --git a/packages/cli/src/services/redis-client.service.ts b/packages/cli/src/services/redis-client.service.ts index b0060c6d96f..c0c1eb69d1f 100644 --- a/packages/cli/src/services/redis-client.service.ts +++ b/packages/cli/src/services/redis-client.service.ts @@ -1,4 +1,4 @@ -import { Logger } from '@n8n/backend-common'; +import { inTest, Logger } from '@n8n/backend-common'; import { GlobalConfig } from '@n8n/config'; import { Debounce } from '@n8n/decorators'; import { Service } from '@n8n/di'; @@ -43,6 +43,13 @@ export class RedisClientService extends TypedEmitter { /** Whether any client has lost connection to Redis. */ private lostConnection = false; + /** + * Whether to exit the process when Redis stays unreachable past the timeout. + * Disabled under test so a client that can never reach Redis (e.g. no Redis + * in DB-test runs) does not call `process.exit` and kill the test worker. + */ + private readonly exitOnRedisUnreachable = !inTest; + constructor( private readonly logger: Logger, private readonly globalConfig: GlobalConfig, @@ -243,8 +250,10 @@ export class RedisClientService extends TypedEmitter { if (cumulativeTimeout > this.config.maxTimeout) { const maxTimeout = Math.round(this.config.maxTimeout / 1000) + 's'; this.logger.error(`Unable to connect to Redis after trying to connect for ${maxTimeout}`); - this.logger.error('Exiting process due to Redis connection error'); - process.exit(1); + if (this.exitOnRedisUnreachable) { + this.logger.error('Exiting process due to Redis connection error'); + process.exit(1); + } } }