diff --git a/packages/cli/src/scaling/__tests__/multi-main-setup.ee.test.ts b/packages/cli/src/scaling/__tests__/multi-main-setup.ee.test.ts new file mode 100644 index 00000000000..80f828440ec --- /dev/null +++ b/packages/cli/src/scaling/__tests__/multi-main-setup.ee.test.ts @@ -0,0 +1,110 @@ +import { mockLogger } from '@n8n/backend-test-utils'; +import type { GlobalConfig } from '@n8n/config'; +import { MultiMainMetadata } from '@n8n/decorators'; +import { mock } from 'jest-mock-extended'; +import type { ErrorReporter, InstanceSettings } from 'n8n-core'; + +import type { Publisher } from '@/scaling/pubsub/publisher.service'; +import type { RedisClientService } from '@/services/redis-client.service'; + +import { MultiMainSetup } from '../multi-main-setup.ee'; + +describe('MultiMainSetup', () => { + const hostId = 'main-n8n-main-0'; + + const logger = mockLogger(); + const publisher = mock(); + const redisClientService = mock(); + const errorReporter = mock(); + const metadata = new MultiMainMetadata(); + + const globalConfig = mock({ + redis: { prefix: 'n8n' }, + multiMainSetup: { ttl: 10, interval: 3, enabled: true }, + }); + + redisClientService.toValidPrefix.mockReturnValue('n8n'); + + let instanceSettings: InstanceSettings; + let multiMainSetup: MultiMainSetup; + + beforeEach(() => { + instanceSettings = mock({ hostId, isLeader: false }); + multiMainSetup = new MultiMainSetup( + logger, + instanceSettings, + publisher, + redisClientService, + globalConfig, + metadata, + errorReporter, + ); + jest.clearAllMocks(); + }); + + describe('checkLeader', () => { + beforeEach(async () => { + await multiMainSetup.init(); + }); + + it('should emit `leader-takeover` when Redis has own `hostId` but instance thinks it is follower', async () => { + publisher.get.mockResolvedValue(hostId); + const emit = jest.spyOn(multiMainSetup, 'emit'); + + // @ts-expect-error - private method + await multiMainSetup.checkLeader(); + + expect(instanceSettings.markAsLeader).toHaveBeenCalled(); + expect(emit).toHaveBeenCalledWith('leader-takeover'); + }); + + it('should not emit `leader-takeover` when already leader', async () => { + publisher.get.mockResolvedValue(hostId); + Object.defineProperty(instanceSettings, 'isLeader', { get: () => true }); + const emit = jest.spyOn(multiMainSetup, 'emit'); + + // @ts-expect-error - private method + await multiMainSetup.checkLeader(); + + expect(instanceSettings.markAsLeader).not.toHaveBeenCalled(); + expect(emit).not.toHaveBeenCalledWith('leader-takeover'); + expect(publisher.setExpiration).toHaveBeenCalled(); + }); + + it('should emit `leader-stepdown` when another instance is leader', async () => { + publisher.get.mockResolvedValue('main-n8n-main-1'); + Object.defineProperty(instanceSettings, 'isLeader', { get: () => true }); + const emit = jest.spyOn(multiMainSetup, 'emit'); + + // @ts-expect-error - private method + await multiMainSetup.checkLeader(); + + expect(instanceSettings.markAsFollower).toHaveBeenCalled(); + expect(emit).toHaveBeenCalledWith('leader-stepdown'); + }); + + it('should not emit `leader-stepdown` when already a follower', async () => { + publisher.get.mockResolvedValue('main-n8n-main-1'); + const emit = jest.spyOn(multiMainSetup, 'emit'); + + // @ts-expect-error - private method + await multiMainSetup.checkLeader(); + + expect(emit).not.toHaveBeenCalledWith('leader-stepdown'); + }); + + it('should attempt to become leader when leadership is vacant', async () => { + publisher.get.mockResolvedValue(null); + publisher.setIfNotExists.mockResolvedValue(true); + const emit = jest.spyOn(multiMainSetup, 'emit'); + + // @ts-expect-error - private method + await multiMainSetup.checkLeader(); + + expect(instanceSettings.markAsFollower).toHaveBeenCalled(); + expect(emit).toHaveBeenCalledWith('leader-stepdown'); + expect(emit).toHaveBeenCalledWith('leader-takeover'); + expect(instanceSettings.markAsLeader).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/cli/src/scaling/multi-main-setup.ee.ts b/packages/cli/src/scaling/multi-main-setup.ee.ts index b06dd89da1a..a1290c87f17 100644 --- a/packages/cli/src/scaling/multi-main-setup.ee.ts +++ b/packages/cli/src/scaling/multi-main-setup.ee.ts @@ -84,8 +84,11 @@ export class MultiMainSetup extends TypedEmitter { shouldReport: true, }, ); + + this.instanceSettings.markAsLeader(); + + this.emit('leader-takeover'); } - this.instanceSettings.markAsLeader(); this.logger.debug(`[Instance ID ${hostId}] Leader is this instance`);