From 1c77d6597f6474fea879efa05b415c2198dceaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 2 Nov 2023 14:16:22 +0100 Subject: [PATCH] refactor(core): Ensure only leader handles licensing in multi-main scenario (#7558) https://linear.app/n8n/issue/PAY-953/ensure-only-main-instance-leader-handles-licensing --- packages/cli/src/License.ts | 11 ++++ packages/cli/src/commands/BaseCommand.ts | 11 ++++ .../integration/commands/start.cmd.test.ts | 53 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 packages/cli/test/integration/commands/start.cmd.test.ts diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 81fceac16a..e99c387a86 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -113,6 +113,17 @@ export class License { async onFeatureChange(_features: TFeatures): Promise { if (config.getEnv('executions.mode') === 'queue') { + if (config.getEnv('leaderSelection.enabled')) { + const { MultiMainInstancePublisher } = await import( + '@/services/orchestration/main/MultiMainInstance.publisher.ee' + ); + + if (Container.get(MultiMainInstancePublisher).isFollower) { + this.logger.debug('Instance is follower, skipping sending of reloadLicense command...'); + return; + } + } + if (!this.redisPublisher) { this.logger.debug('Initializing Redis publisher for License Service'); this.redisPublisher = await Container.get(RedisService).getPubSubPublisher(); diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 868f26d825..cbcbb3d70b 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -243,6 +243,17 @@ export abstract class BaseCommand extends Command { } async initLicense(): Promise { + if (config.getEnv('executions.mode') === 'queue' && config.getEnv('leaderSelection.enabled')) { + const { MultiMainInstancePublisher } = await import( + '@/services/orchestration/main/MultiMainInstance.publisher.ee' + ); + + if (Container.get(MultiMainInstancePublisher).isFollower) { + this.logger.debug('Instance is follower, skipping license initialization...'); + return; + } + } + const license = Container.get(License); await license.init(this.instanceType ?? 'main'); diff --git a/packages/cli/test/integration/commands/start.cmd.test.ts b/packages/cli/test/integration/commands/start.cmd.test.ts new file mode 100644 index 0000000000..47158a1c11 --- /dev/null +++ b/packages/cli/test/integration/commands/start.cmd.test.ts @@ -0,0 +1,53 @@ +import * as Config from '@oclif/config'; + +import { mockInstance } from '../shared/utils'; + +import { Start } from '@/commands/start'; +import { BaseCommand } from '@/commands/BaseCommand'; +import config from '@/config'; +import { License } from '@/License'; + +import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; +import { MultiMainInstancePublisher } from '@/services/orchestration/main/MultiMainInstance.publisher.ee'; +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { WorkflowHistoryManager } from '@/workflows/workflowHistory/workflowHistoryManager.ee'; +import { RedisService } from '@/services/redis.service'; +import { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher'; +import { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber'; +import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; + +const oclifConfig: Config.IConfig = new Config.Config({ root: __dirname }); + +beforeAll(() => { + mockInstance(ExternalSecretsManager); + mockInstance(ActiveWorkflowRunner); + mockInstance(WorkflowHistoryManager); + mockInstance(RedisService); + mockInstance(RedisServicePubSubPublisher); + mockInstance(RedisServicePubSubSubscriber); + mockInstance(MultiMainInstancePublisher); + mockInstance(OrchestrationHandlerMainService); +}); + +afterEach(() => { + config.load(config.default); + jest.restoreAllMocks(); +}); + +test('should not init license if instance is follower in multi-main scenario', async () => { + config.set('executions.mode', 'queue'); + config.set('leaderSelection.enabled', true); + + jest.spyOn(MultiMainInstancePublisher.prototype, 'isFollower', 'get').mockReturnValue(true); + jest.spyOn(BaseCommand.prototype, 'init').mockImplementation(async () => {}); + + const licenseMock = mockInstance(License, { + isMultipleMainInstancesLicensed: jest.fn().mockReturnValue(true), + }); + + const startCmd = new Start([], oclifConfig); + + await startCmd.init(); + + expect(licenseMock.init).not.toHaveBeenCalled(); +});