refactor(core): Simplify OrchestrationService (no-changelog) (#8364)

This commit is contained in:
Iván Ovejero 2024-01-22 11:16:29 +01:00 committed by GitHub
parent 2ccb754e52
commit f35d4fcbd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 323 additions and 315 deletions

View file

@ -47,7 +47,7 @@ import { ExternalHooks } from '@/ExternalHooks';
import { WebhookService } from './services/webhook.service'; import { WebhookService } from './services/webhook.service';
import { Logger } from './Logger'; import { Logger } from './Logger';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { ActivationErrorsService } from '@/ActivationErrors.service'; import { ActivationErrorsService } from '@/ActivationErrors.service';
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service'; import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service'; import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
@ -72,7 +72,7 @@ export class ActiveWorkflowRunner {
private readonly nodeTypes: NodeTypes, private readonly nodeTypes: NodeTypes,
private readonly webhookService: WebhookService, private readonly webhookService: WebhookService,
private readonly workflowRepository: WorkflowRepository, private readonly workflowRepository: WorkflowRepository,
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
private readonly activationErrorsService: ActivationErrorsService, private readonly activationErrorsService: ActivationErrorsService,
private readonly executionService: ExecutionService, private readonly executionService: ExecutionService,
private readonly workflowStaticDataService: WorkflowStaticDataService, private readonly workflowStaticDataService: WorkflowStaticDataService,
@ -80,7 +80,7 @@ export class ActiveWorkflowRunner {
) {} ) {}
async init() { async init() {
await this.multiMainSetup.init(); await this.orchestrationService.init();
await this.addActiveWorkflows('init'); await this.addActiveWorkflows('init');
@ -470,25 +470,23 @@ export class ActiveWorkflowRunner {
if (dbWorkflows.length === 0) return; if (dbWorkflows.length === 0) return;
this.logger.info(' ================================'); if (this.orchestrationService.isLeader) {
this.logger.info(' Start Active Workflows:'); this.logger.info(' ================================');
this.logger.info(' ================================'); this.logger.info(' Start Active Workflows:');
this.logger.info(' ================================');
}
for (const dbWorkflow of dbWorkflows) { for (const dbWorkflow of dbWorkflows) {
this.logger.info(` - ${dbWorkflow.display()}`);
this.logger.debug(`Initializing active workflow ${dbWorkflow.display()} (startup)`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
try { try {
await this.add(dbWorkflow.id, activationMode, dbWorkflow); const wasActivated = await this.add(dbWorkflow.id, activationMode, dbWorkflow);
this.logger.verbose(`Successfully started workflow ${dbWorkflow.display()}`, { if (wasActivated) {
workflowName: dbWorkflow.name, this.logger.verbose(`Successfully started workflow ${dbWorkflow.display()}`, {
workflowId: dbWorkflow.id, workflowName: dbWorkflow.name,
}); workflowId: dbWorkflow.id,
this.logger.info(' => Started'); });
this.logger.info(' => Started');
}
} catch (error) { } catch (error) {
ErrorReporter.error(error); ErrorReporter.error(error);
this.logger.info( this.logger.info(
@ -571,16 +569,18 @@ export class ActiveWorkflowRunner {
* again, and the new leader should take over the triggers and pollers that stopped * again, and the new leader should take over the triggers and pollers that stopped
* running when the former leader became unresponsive. * running when the former leader became unresponsive.
*/ */
if (this.multiMainSetup.isEnabled) { if (this.orchestrationService.isMultiMainSetupEnabled) {
if (activationMode !== 'leadershipChange') { if (activationMode !== 'leadershipChange') {
shouldAddWebhooks = this.multiMainSetup.isLeader; shouldAddWebhooks = this.orchestrationService.isLeader;
shouldAddTriggersAndPollers = this.multiMainSetup.isLeader; shouldAddTriggersAndPollers = this.orchestrationService.isLeader;
} else { } else {
shouldAddWebhooks = false; shouldAddWebhooks = false;
shouldAddTriggersAndPollers = this.multiMainSetup.isLeader; shouldAddTriggersAndPollers = this.orchestrationService.isLeader;
} }
} }
const shouldActivate = shouldAddWebhooks || shouldAddTriggersAndPollers;
try { try {
const dbWorkflow = existingWorkflow ?? (await this.workflowRepository.findById(workflowId)); const dbWorkflow = existingWorkflow ?? (await this.workflowRepository.findById(workflowId));
@ -588,6 +588,14 @@ export class ActiveWorkflowRunner {
throw new WorkflowActivationError(`Failed to find workflow with ID "${workflowId}"`); throw new WorkflowActivationError(`Failed to find workflow with ID "${workflowId}"`);
} }
if (shouldActivate) {
this.logger.info(` - ${dbWorkflow.display()}`);
this.logger.debug(`Initializing active workflow ${dbWorkflow.display()} (startup)`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
}
workflow = new Workflow({ workflow = new Workflow({
id: dbWorkflow.id, id: dbWorkflow.id,
name: dbWorkflow.name, name: dbWorkflow.name,
@ -644,6 +652,8 @@ export class ActiveWorkflowRunner {
// If for example webhooks get created it sometimes has to save the // If for example webhooks get created it sometimes has to save the
// id of them in the static data. So make sure that data gets persisted. // id of them in the static data. So make sure that data gets persisted.
await this.workflowStaticDataService.saveStaticData(workflow); await this.workflowStaticDataService.saveStaticData(workflow);
return shouldActivate;
} }
/** /**
@ -804,7 +814,7 @@ export class ActiveWorkflowRunner {
); );
if (workflow.getTriggerNodes().length !== 0 || workflow.getPollNodes().length !== 0) { if (workflow.getTriggerNodes().length !== 0 || workflow.getPollNodes().length !== 0) {
this.logger.debug(`Adding triggers and pollers for workflow "${dbWorkflow.display()}"`); this.logger.debug(`Adding triggers and pollers for workflow ${dbWorkflow.display()}`);
await this.activeWorkflows.add( await this.activeWorkflows.add(
workflow.id, workflow.id,

View file

@ -16,7 +16,7 @@ import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { updateIntervalTime } from './externalSecretsHelper.ee'; import { updateIntervalTime } from './externalSecretsHelper.ee';
import { ExternalSecretsProviders } from './ExternalSecretsProviders.ee'; import { ExternalSecretsProviders } from './ExternalSecretsProviders.ee';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationService } from '@/services/orchestration.service';
@Service() @Service()
export class ExternalSecretsManager { export class ExternalSecretsManager {
@ -79,7 +79,7 @@ export class ExternalSecretsManager {
} }
async broadcastReloadExternalSecretsProviders() { async broadcastReloadExternalSecretsProviders() {
await Container.get(SingleMainSetup).broadcastReloadExternalSecretsProviders(); await Container.get(OrchestrationService).publish('reloadExternalSecretsProviders');
} }
private decryptSecretsSettings(value: string): ExternalSecretsSettings { private decryptSecretsSettings(value: string): ExternalSecretsSettings {

View file

@ -16,7 +16,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces'; import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher'; import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
import { RedisService } from './services/redis.service'; import { RedisService } from './services/redis.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { OnShutdown } from '@/decorators/OnShutdown'; import { OnShutdown } from '@/decorators/OnShutdown';
type FeatureReturnType = Partial< type FeatureReturnType = Partial<
@ -36,7 +36,7 @@ export class License {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
private readonly settingsRepository: SettingsRepository, private readonly settingsRepository: SettingsRepository,
private readonly workflowRepository: WorkflowRepository, private readonly workflowRepository: WorkflowRepository,
) {} ) {}
@ -51,8 +51,6 @@ export class License {
return; return;
} }
await this.multiMainSetup.init();
const isMainInstance = instanceType === 'main'; const isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl'); const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled'); const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
@ -123,16 +121,19 @@ export class License {
| boolean | boolean
| undefined; | undefined;
this.multiMainSetup.setLicensed(isMultiMainLicensed ?? false); this.orchestrationService.setMultiMainSetupLicensed(isMultiMainLicensed ?? false);
if (this.multiMainSetup.isEnabled && this.multiMainSetup.isFollower) { if (
this.orchestrationService.isMultiMainSetupEnabled &&
this.orchestrationService.isFollower
) {
this.logger.debug( this.logger.debug(
'[Multi-main setup] Instance is follower, skipping sending of "reloadLicense" command...', '[Multi-main setup] Instance is follower, skipping sending of "reloadLicense" command...',
); );
return; return;
} }
if (this.multiMainSetup.isEnabled && !isMultiMainLicensed) { if (this.orchestrationService.isMultiMainSetupEnabled && !isMultiMainLicensed) {
this.logger.debug( this.logger.debug(
'[Multi-main setup] License changed with no support for multi-main setup - no new followers will be allowed to init. To restore multi-main setup, please upgrade to a license that supporst this feature.', '[Multi-main setup] License changed with no support for multi-main setup - no new followers will be allowed to init. To restore multi-main setup, please upgrade to a license that supporst this feature.',
); );

View file

@ -101,7 +101,7 @@ import { CollaborationService } from './collaboration/collaboration.service';
import { RoleController } from './controllers/role.controller'; import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error'; import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NotFoundError } from './errors/response-errors/not-found.error'; import { NotFoundError } from './errors/response-errors/not-found.error';
import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowSharingService } from './workflows/workflowSharing.service'; import { WorkflowSharingService } from './workflows/workflowSharing.service';
const exec = promisify(callbackExec); const exec = promisify(callbackExec);
@ -252,7 +252,10 @@ export class Server extends AbstractServer {
ExecutionsController, ExecutionsController,
]; ];
if (process.env.NODE_ENV !== 'production' && Container.get(MultiMainSetup).isEnabled) { if (
process.env.NODE_ENV !== 'production' &&
Container.get(OrchestrationService).isMultiMainSetupEnabled
) {
const { DebugController } = await import('@/controllers/debug.controller'); const { DebugController } = await import('@/controllers/debug.controller');
controllers.push(DebugController); controllers.push(DebugController);
} }

View file

@ -25,7 +25,7 @@ import * as NodeExecuteFunctions from 'n8n-core';
import { removeTrailingSlash } from './utils'; import { removeTrailingSlash } from './utils';
import type { TestWebhookRegistration } from '@/services/test-webhook-registrations.service'; import type { TestWebhookRegistration } from '@/services/test-webhook-registrations.service';
import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service'; import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service';
import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@Service() @Service()
@ -34,7 +34,7 @@ export class TestWebhooks implements IWebhookManager {
private readonly push: Push, private readonly push: Push,
private readonly nodeTypes: NodeTypes, private readonly nodeTypes: NodeTypes,
private readonly registrations: TestWebhookRegistrationsService, private readonly registrations: TestWebhookRegistrationsService,
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
) {} ) {}
private timeouts: { [webhookKey: string]: NodeJS.Timeout } = {}; private timeouts: { [webhookKey: string]: NodeJS.Timeout } = {};
@ -144,12 +144,12 @@ export class TestWebhooks implements IWebhookManager {
* the handler process commands the creator process to clear its test webhooks. * the handler process commands the creator process to clear its test webhooks.
*/ */
if ( if (
this.multiMainSetup.isEnabled && this.orchestrationService.isMultiMainSetupEnabled &&
sessionId && sessionId &&
!this.push.getBackend().hasSessionId(sessionId) !this.push.getBackend().hasSessionId(sessionId)
) { ) {
const payload = { webhookKey: key, workflowEntity, sessionId }; const payload = { webhookKey: key, workflowEntity, sessionId };
void this.multiMainSetup.publish('clear-test-webhooks', payload); void this.orchestrationService.publish('clear-test-webhooks', payload);
return; return;
} }

View file

@ -22,10 +22,9 @@ import { BaseCommand } from './BaseCommand';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import type { IConfig } from '@oclif/config'; import type { IConfig } from '@oclif/config';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationService } from '@/services/orchestration.service';
import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service';
import { PruningService } from '@/services/pruning.service'; import { PruningService } from '@/services/pruning.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository';
@ -104,10 +103,10 @@ export class Start extends BaseCommand {
await this.externalHooks?.run('n8n.stop', []); await this.externalHooks?.run('n8n.stop', []);
if (Container.get(MultiMainSetup).isEnabled) { if (Container.get(OrchestrationService).isMultiMainSetupEnabled) {
await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows(); await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows();
await Container.get(MultiMainSetup).shutdown(); await Container.get(OrchestrationService).shutdown();
} }
await Container.get(InternalHooks).onN8nStop(); await Container.get(InternalHooks).onN8nStop();
@ -216,43 +215,48 @@ export class Start extends BaseCommand {
async initOrchestration() { async initOrchestration() {
if (config.getEnv('executions.mode') !== 'queue') return; if (config.getEnv('executions.mode') !== 'queue') return;
// queue mode in single-main scenario if (
config.getEnv('multiMainSetup.enabled') &&
if (!config.getEnv('multiMainSetup.enabled')) { !Container.get(License).isMultipleMainInstancesLicensed()
await Container.get(SingleMainSetup).init(); ) {
await Container.get(OrchestrationHandlerMainService).init();
return;
}
// queue mode in multi-main scenario
if (!Container.get(License).isMultipleMainInstancesLicensed()) {
throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES); throw new FeatureNotLicensedError(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
} }
const orchestrationService = Container.get(OrchestrationService);
await orchestrationService.init();
await Container.get(OrchestrationHandlerMainService).init(); await Container.get(OrchestrationHandlerMainService).init();
const multiMainSetup = Container.get(MultiMainSetup); if (!orchestrationService.isMultiMainSetupEnabled) return;
await multiMainSetup.init(); orchestrationService.multiMainSetup
.addListener('leadershipChange', async () => {
if (orchestrationService.isLeader) {
this.logger.debug('[Leadership change] Clearing all activation errors...');
multiMainSetup.on('leadershipChange', async () => { await this.activeWorkflowRunner.clearAllActivationErrors();
if (multiMainSetup.isLeader) {
this.logger.debug('[Leadership change] Clearing all activation errors...');
await this.activeWorkflowRunner.clearAllActivationErrors(); this.logger.debug(
'[Leadership change] Adding all trigger- and poller-based workflows...',
);
this.logger.debug('[Leadership change] Adding all trigger- and poller-based workflows...'); await this.activeWorkflowRunner.addAllTriggerAndPollerBasedWorkflows();
} else {
this.logger.debug(
'[Leadership change] Removing all trigger- and poller-based workflows...',
);
await this.activeWorkflowRunner.addAllTriggerAndPollerBasedWorkflows(); await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows();
} else { }
})
.addListener('leadershipVacant', async () => {
this.logger.debug( this.logger.debug(
'[Leadership change] Removing all trigger- and poller-based workflows...', '[Leadership vacant] Removing all trigger- and poller-based workflows...',
); );
await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows(); await this.activeWorkflowRunner.removeAllTriggerAndPollerBasedWorkflows();
} });
});
} }
async run() { async run() {
@ -361,27 +365,27 @@ export class Start extends BaseCommand {
async initPruning() { async initPruning() {
this.pruningService = Container.get(PruningService); this.pruningService = Container.get(PruningService);
if (this.pruningService.isPruningEnabled()) { this.pruningService.startPruning();
this.pruningService.startPruning();
}
if (config.getEnv('executions.mode') === 'queue' && config.getEnv('multiMainSetup.enabled')) { if (config.getEnv('executions.mode') !== 'queue') return;
const multiMainSetup = Container.get(MultiMainSetup);
await multiMainSetup.init(); const orchestrationService = Container.get(OrchestrationService);
multiMainSetup.on('leadershipChange', async () => { await orchestrationService.init();
if (multiMainSetup.isLeader) {
if (this.pruningService.isPruningEnabled()) { if (!orchestrationService.isMultiMainSetupEnabled) return;
this.pruningService.startPruning();
} orchestrationService.multiMainSetup
.addListener('leadershipChange', async () => {
if (orchestrationService.isLeader) {
this.pruningService.startPruning();
} else { } else {
if (this.pruningService.isPruningEnabled()) { this.pruningService.stopPruning();
this.pruningService.stopPruning();
}
} }
})
.addListener('leadershipVacant', () => {
this.pruningService.stopPruning();
}); });
}
} }
async catch(error: Error) { async catch(error: Error) {

View file

@ -1,19 +1,19 @@
import { Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
@RestController('/debug') @RestController('/debug')
export class DebugController { export class DebugController {
constructor( constructor(
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly workflowRepository: WorkflowRepository, private readonly workflowRepository: WorkflowRepository,
) {} ) {}
@Get('/multi-main-setup') @Get('/multi-main-setup')
async getMultiMainSetupDetails() { async getMultiMainSetupDetails() {
const leaderKey = await this.multiMainSetup.fetchLeaderKey(); const leaderKey = await this.orchestrationService.multiMainSetup.fetchLeaderKey();
const triggersAndPollers = await this.workflowRepository.findIn( const triggersAndPollers = await this.workflowRepository.findIn(
this.activeWorkflowRunner.allActiveInMemory(), this.activeWorkflowRunner.allActiveInMemory(),
@ -24,9 +24,9 @@ export class DebugController {
const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors(); const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors();
return { return {
instanceId: this.multiMainSetup.instanceId, instanceId: this.orchestrationService.instanceId,
leaderKey, leaderKey,
isLeader: this.multiMainSetup.isLeader, isLeader: this.orchestrationService.isLeader,
activeWorkflows: { activeWorkflows: {
webhooks, // webhook-based active workflows webhooks, // webhook-based active workflows
triggersAndPollers, // poller- and trigger-based active workflows triggersAndPollers, // poller- and trigger-based active workflows

View file

@ -1,13 +1,13 @@
import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators'; import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators';
import { OrchestrationRequest } from '@/requests'; import { OrchestrationRequest } from '@/requests';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationService } from '@/services/orchestration.service';
import { License } from '@/License'; import { License } from '@/License';
@Authorized() @Authorized()
@RestController('/orchestration') @RestController('/orchestration')
export class OrchestrationController { export class OrchestrationController {
constructor( constructor(
private readonly singleMainSetup: SingleMainSetup, private readonly orchestrationService: OrchestrationService,
private readonly licenseService: License, private readonly licenseService: License,
) {} ) {}
@ -20,20 +20,20 @@ export class OrchestrationController {
async getWorkersStatus(req: OrchestrationRequest.Get) { async getWorkersStatus(req: OrchestrationRequest.Get) {
if (!this.licenseService.isWorkerViewLicensed()) return; if (!this.licenseService.isWorkerViewLicensed()) return;
const id = req.params.id; const id = req.params.id;
return await this.singleMainSetup.getWorkerStatus(id); return await this.orchestrationService.getWorkerStatus(id);
} }
@RequireGlobalScope('orchestration:read') @RequireGlobalScope('orchestration:read')
@Post('/worker/status') @Post('/worker/status')
async getWorkersStatusAll() { async getWorkersStatusAll() {
if (!this.licenseService.isWorkerViewLicensed()) return; if (!this.licenseService.isWorkerViewLicensed()) return;
return await this.singleMainSetup.getWorkerStatus(); return await this.orchestrationService.getWorkerStatus();
} }
@RequireGlobalScope('orchestration:list') @RequireGlobalScope('orchestration:list')
@Post('/worker/ids') @Post('/worker/ids')
async getWorkerIdsAll() { async getWorkerIdsAll() {
if (!this.licenseService.isWorkerViewLicensed()) return; if (!this.licenseService.isWorkerViewLicensed()) return;
return await this.singleMainSetup.getWorkerIds(); return await this.orchestrationService.getWorkerIds();
} }
} }

View file

@ -32,7 +32,7 @@ import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions'; import type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions';
import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers'; import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationService } from '@/services/orchestration.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { EventDestinationsRepository } from '@db/repositories/eventDestinations.repository'; import { EventDestinationsRepository } from '@db/repositories/eventDestinations.repository';
@ -207,7 +207,7 @@ export class MessageEventBus extends EventEmitter {
this.destinations[destination.getId()] = destination; this.destinations[destination.getId()] = destination;
this.destinations[destination.getId()].startListening(); this.destinations[destination.getId()].startListening();
if (notifyWorkers) { if (notifyWorkers) {
await Container.get(SingleMainSetup).broadcastRestartEventbusAfterDestinationUpdate(); await Container.get(OrchestrationService).publish('restartEventBus');
} }
return destination; return destination;
} }
@ -233,7 +233,7 @@ export class MessageEventBus extends EventEmitter {
delete this.destinations[id]; delete this.destinations[id];
} }
if (notifyWorkers) { if (notifyWorkers) {
await Container.get(SingleMainSetup).broadcastRestartEventbusAfterDestinationUpdate(); await Container.get(OrchestrationService).publish('restartEventBus');
} }
return result; return result;
} }

View file

@ -3,7 +3,7 @@ import { assert, jsonStringify } from 'n8n-workflow';
import type { IPushDataType } from '@/Interfaces'; import type { IPushDataType } from '@/Interfaces';
import type { Logger } from '@/Logger'; import type { Logger } from '@/Logger';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import type { OrchestrationService } from '@/services/orchestration.service';
/** /**
* Abstract class for two-way push communication. * Abstract class for two-way push communication.
@ -21,7 +21,7 @@ export abstract class AbstractPush<T> extends EventEmitter {
constructor( constructor(
protected readonly logger: Logger, protected readonly logger: Logger,
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
) { ) {
super(); super();
} }
@ -84,10 +84,10 @@ export abstract class AbstractPush<T> extends EventEmitter {
* the webhook. If so, the handler process commands the creator process to * the webhook. If so, the handler process commands the creator process to
* relay the former's execution lifecyle events to the creator's frontend. * relay the former's execution lifecyle events to the creator's frontend.
*/ */
if (this.multiMainSetup.isEnabled && !this.hasSessionId(sessionId)) { if (this.orchestrationService.isMultiMainSetupEnabled && !this.hasSessionId(sessionId)) {
const payload = { type, args: data, sessionId }; const payload = { type, args: data, sessionId };
void this.multiMainSetup.publish('relay-execution-lifecycle-event', payload); void this.orchestrationService.publish('relay-execution-lifecycle-event', payload);
return; return;
} }

View file

@ -4,7 +4,7 @@ import { Logger } from '@/Logger';
import { AbstractPush } from './abstract.push'; import { AbstractPush } from './abstract.push';
import type { PushRequest, PushResponse } from './types'; import type { PushRequest, PushResponse } from './types';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
type Connection = { req: PushRequest; res: PushResponse }; type Connection = { req: PushRequest; res: PushResponse };
@ -14,8 +14,8 @@ export class SSEPush extends AbstractPush<Connection> {
readonly connections: Record<string, Connection> = {}; readonly connections: Record<string, Connection> = {};
constructor(logger: Logger, multiMainSetup: MultiMainSetup) { constructor(logger: Logger, orchestrationService: OrchestrationService) {
super(logger, multiMainSetup); super(logger, orchestrationService);
this.channel.on('disconnect', (channel, { req }) => { this.channel.on('disconnect', (channel, { req }) => {
this.remove(req?.query?.sessionId); this.remove(req?.query?.sessionId);

View file

@ -3,7 +3,7 @@ import { Service } from 'typedi';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { AbstractPush } from './abstract.push'; import { AbstractPush } from './abstract.push';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
function heartbeat(this: WebSocket) { function heartbeat(this: WebSocket) {
this.isAlive = true; this.isAlive = true;
@ -11,8 +11,8 @@ function heartbeat(this: WebSocket) {
@Service() @Service()
export class WebSocketPush extends AbstractPush<WebSocket> { export class WebSocketPush extends AbstractPush<WebSocket> {
constructor(logger: Logger, multiMainSetup: MultiMainSetup) { constructor(logger: Logger, orchestrationService: OrchestrationService) {
super(logger, multiMainSetup); super(logger, orchestrationService);
// Ping all connected clients every 60 seconds // Ping all connected clients every 60 seconds
setInterval(() => this.pingAll(), 60 * 1000); setInterval(() => this.pingAll(), 60 * 1000);

View file

@ -1,55 +0,0 @@
import Container from 'typedi';
import { RedisService } from './redis.service';
import type { RedisServicePubSubPublisher } from './redis/RedisServicePubSubPublisher';
import config from '@/config';
import { EventEmitter } from 'node:events';
export abstract class OrchestrationService extends EventEmitter {
protected isInitialized = false;
protected queueModeId: string;
redisPublisher: RedisServicePubSubPublisher;
readonly redisService: RedisService;
get isQueueMode(): boolean {
return config.get('executions.mode') === 'queue';
}
get isMainInstance(): boolean {
return config.get('generic.instanceType') === 'main';
}
get isWebhookInstance(): boolean {
return config.get('generic.instanceType') === 'webhook';
}
get isWorkerInstance(): boolean {
return config.get('generic.instanceType') === 'worker';
}
constructor() {
super();
this.redisService = Container.get(RedisService);
this.queueModeId = config.getEnv('redis.queueModeId');
}
sanityCheck(): boolean {
return this.isInitialized && this.isQueueMode;
}
async init() {
await this.initPublisher();
this.isInitialized = true;
}
async shutdown() {
await this.redisPublisher?.destroy();
this.isInitialized = false;
}
protected async initPublisher() {
this.redisPublisher = await this.redisService.getPubSubPublisher();
}
}

View file

@ -0,0 +1,121 @@
import { Service } from 'typedi';
import { Logger } from '@/Logger';
import config from '@/config';
import type { RedisServicePubSubPublisher } from './redis/RedisServicePubSubPublisher';
import type { RedisServiceBaseCommand, RedisServiceCommand } from './redis/RedisServiceCommands';
import { RedisService } from './redis.service';
import { MultiMainSetup } from './orchestration/main/MultiMainSetup.ee';
@Service()
export class OrchestrationService {
constructor(
private readonly logger: Logger,
private readonly redisService: RedisService,
readonly multiMainSetup: MultiMainSetup,
) {}
protected isInitialized = false;
private isMultiMainSetupLicensed = false;
setMultiMainSetupLicensed(newState: boolean) {
this.isMultiMainSetupLicensed = newState;
}
get isMultiMainSetupEnabled() {
return (
config.getEnv('executions.mode') === 'queue' &&
config.getEnv('multiMainSetup.enabled') &&
config.getEnv('generic.instanceType') === 'main' &&
this.isMultiMainSetupLicensed
);
}
redisPublisher: RedisServicePubSubPublisher;
get instanceId() {
return config.getEnv('redis.queueModeId');
}
get isLeader() {
return config.getEnv('multiMainSetup.instanceType') === 'leader';
}
get isFollower() {
return config.getEnv('multiMainSetup.instanceType') !== 'leader';
}
sanityCheck() {
return this.isInitialized && config.get('executions.mode') === 'queue';
}
async init() {
if (this.isInitialized) return;
if (config.get('executions.mode') === 'queue') await this.initPublisher();
if (this.isMultiMainSetupEnabled) {
await this.multiMainSetup.init();
} else {
config.set('multiMainSetup.instanceType', 'leader');
}
this.isInitialized = true;
}
async shutdown() {
if (!this.isInitialized) return;
if (this.isMultiMainSetupEnabled) await this.multiMainSetup.shutdown();
await this.redisPublisher.destroy();
this.isInitialized = false;
}
// ----------------------------------
// pubsub
// ----------------------------------
protected async initPublisher() {
this.redisPublisher = await this.redisService.getPubSubPublisher();
}
async publish(command: RedisServiceCommand, data?: unknown) {
if (!this.sanityCheck()) return;
const payload = data as RedisServiceBaseCommand['payload'];
this.logger.debug(`[Instance ID ${this.instanceId}] Publishing command "${command}"`, payload);
await this.redisPublisher.publishToCommandChannel({ command, payload });
}
// ----------------------------------
// workers status
// ----------------------------------
async getWorkerStatus(id?: string) {
if (!this.sanityCheck()) return;
const command = 'getStatus';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({
command,
targets: id ? [id] : undefined,
});
}
async getWorkerIds() {
if (!this.sanityCheck()) return;
const command = 'getId';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({ command });
}
}

View file

@ -1,43 +1,23 @@
import { EventEmitter } from 'node:events';
import config from '@/config'; import config from '@/config';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { TIME } from '@/constants'; import { TIME } from '@/constants';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { getRedisPrefix } from '@/services/redis/RedisServiceHelper'; import { getRedisPrefix } from '@/services/redis/RedisServiceHelper';
import { ErrorReporterProxy as EventReporter } from 'n8n-workflow'; import { ErrorReporterProxy as EventReporter } from 'n8n-workflow';
import type { import { Logger } from '@/Logger';
RedisServiceBaseCommand, import { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher';
RedisServiceCommand,
} from '@/services/redis/RedisServiceCommands';
@Service() @Service()
export class MultiMainSetup extends SingleMainSetup { export class MultiMainSetup extends EventEmitter {
private id = this.queueModeId; constructor(
private readonly logger: Logger,
private isLicensed = false; private readonly redisPublisher: RedisServicePubSubPublisher,
) {
get isEnabled() { super();
return (
config.getEnv('executions.mode') === 'queue' &&
config.getEnv('multiMainSetup.enabled') &&
config.getEnv('generic.instanceType') === 'main' &&
this.isLicensed
);
}
get isLeader() {
return config.getEnv('multiMainSetup.instanceType') === 'leader';
}
get isFollower() {
return !this.isLeader;
} }
get instanceId() { get instanceId() {
return this.id; return config.getEnv('redis.queueModeId');
}
setLicensed(newState: boolean) {
this.isLicensed = newState;
} }
private readonly leaderKey = getRedisPrefix() + ':main_instance_leader'; private readonly leaderKey = getRedisPrefix() + ':main_instance_leader';
@ -47,12 +27,6 @@ export class MultiMainSetup extends SingleMainSetup {
private leaderCheckInterval: NodeJS.Timer | undefined; private leaderCheckInterval: NodeJS.Timer | undefined;
async init() { async init() {
if (!this.isEnabled || this.isInitialized) return;
await this.initPublisher();
this.isInitialized = true;
await this.tryBecomeLeader(); // prevent initial wait await this.tryBecomeLeader(); // prevent initial wait
this.leaderCheckInterval = setInterval( this.leaderCheckInterval = setInterval(
@ -64,35 +38,35 @@ export class MultiMainSetup extends SingleMainSetup {
} }
async shutdown() { async shutdown() {
if (!this.isInitialized) return;
clearInterval(this.leaderCheckInterval); clearInterval(this.leaderCheckInterval);
if (this.isLeader) await this.redisPublisher.clear(this.leaderKey); const isLeader = config.getEnv('multiMainSetup.instanceType') === 'leader';
if (isLeader) await this.redisPublisher.clear(this.leaderKey);
} }
private async checkLeader() { private async checkLeader() {
const leaderId = await this.redisPublisher.get(this.leaderKey); const leaderId = await this.redisPublisher.get(this.leaderKey);
if (leaderId === this.id) { if (leaderId === this.instanceId) {
this.logger.debug(`[Instance ID ${this.id}] Leader is this instance`); this.logger.debug(`[Instance ID ${this.instanceId}] Leader is this instance`);
await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl); await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
return; return;
} }
if (leaderId && leaderId !== this.id) { if (leaderId && leaderId !== this.instanceId) {
this.logger.debug(`[Instance ID ${this.id}] Leader is other instance "${leaderId}"`); this.logger.debug(`[Instance ID ${this.instanceId}] Leader is other instance "${leaderId}"`);
if (config.getEnv('multiMainSetup.instanceType') === 'leader') { if (config.getEnv('multiMainSetup.instanceType') === 'leader') {
this.emit('leadershipChange', leaderId); // stop triggers, pruning, etc. config.set('multiMainSetup.instanceType', 'follower');
this.emit('leadershipChange'); // stop triggers, pollers, pruning
EventReporter.report('[Multi-main setup] Leader failed to renew leader key', { EventReporter.report('[Multi-main setup] Leader failed to renew leader key', {
level: 'info', level: 'info',
}); });
config.set('multiMainSetup.instanceType', 'follower');
} }
return; return;
@ -100,42 +74,37 @@ export class MultiMainSetup extends SingleMainSetup {
if (!leaderId) { if (!leaderId) {
this.logger.debug( this.logger.debug(
`[Instance ID ${this.id}] Leadership vacant, attempting to become leader...`, `[Instance ID ${this.instanceId}] Leadership vacant, attempting to become leader...`,
); );
config.set('multiMainSetup.instanceType', 'follower'); config.set('multiMainSetup.instanceType', 'follower');
this.emit('leadershipVacant'); // stop triggers, pollers, pruning
await this.tryBecomeLeader(); await this.tryBecomeLeader();
} }
} }
private async tryBecomeLeader() { private async tryBecomeLeader() {
// this can only succeed if leadership is currently vacant // this can only succeed if leadership is currently vacant
const keySetSuccessfully = await this.redisPublisher.setIfNotExists(this.leaderKey, this.id); const keySetSuccessfully = await this.redisPublisher.setIfNotExists(
this.leaderKey,
this.instanceId,
);
if (keySetSuccessfully) { if (keySetSuccessfully) {
this.logger.debug(`[Instance ID ${this.id}] Leader is now this instance`); this.logger.debug(`[Instance ID ${this.instanceId}] Leader is now this instance`);
config.set('multiMainSetup.instanceType', 'leader'); config.set('multiMainSetup.instanceType', 'leader');
await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl); await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
this.emit('leadershipChange', this.id); this.emit('leadershipChange'); // start triggers, pollers, pruning
} else { } else {
config.set('multiMainSetup.instanceType', 'follower'); config.set('multiMainSetup.instanceType', 'follower');
} }
} }
async publish(command: RedisServiceCommand, data: unknown) {
if (!this.sanityCheck()) return;
const payload = data as RedisServiceBaseCommand['payload'];
this.logger.debug(`[Instance ID ${this.id}] Publishing command "${command}"`, payload);
await this.redisPublisher.publishToCommandChannel({ command, payload });
}
async fetchLeaderKey() { async fetchLeaderKey() {
return await this.redisPublisher.get(this.leaderKey); return await this.redisPublisher.get(this.leaderKey);
} }

View file

@ -1,60 +0,0 @@
import { Logger } from '@/Logger';
import { Service } from 'typedi';
import { OrchestrationService } from '@/services/orchestration.base.service';
/**
* For use in main instance, in single main instance scenario.
*/
@Service()
export class SingleMainSetup extends OrchestrationService {
constructor(protected readonly logger: Logger) {
super();
}
sanityCheck() {
return this.isInitialized && this.isQueueMode && this.isMainInstance;
}
async getWorkerStatus(id?: string) {
if (!this.sanityCheck()) return;
const command = 'getStatus';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({
command,
targets: id ? [id] : undefined,
});
}
async getWorkerIds() {
if (!this.sanityCheck()) return;
const command = 'getId';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({ command });
}
async broadcastRestartEventbusAfterDestinationUpdate() {
if (!this.sanityCheck()) return;
const command = 'restartEventBus';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({ command });
}
async broadcastReloadExternalSecretsProviders() {
if (!this.sanityCheck()) return;
const command = 'reloadExternalSecretsProviders';
this.logger.debug(`Sending "${command}" to command channel`);
await this.redisPublisher.publishToCommandChannel({ command });
}
}

View file

@ -7,7 +7,7 @@ import { License } from '@/License';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { Push } from '@/push'; import { Push } from '@/push';
import { MultiMainSetup } from './MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { TestWebhooks } from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
@ -100,7 +100,7 @@ export async function handleCommandMessageMain(messageString: string) {
versionId, versionId,
}); });
await Container.get(MultiMainSetup).publish('workflowFailedToActivate', { await Container.get(OrchestrationService).publish('workflowFailedToActivate', {
workflowId, workflowId,
errorMessage: error.message, errorMessage: error.message,
}); });

View file

@ -1,9 +1,14 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { OrchestrationService } from '../../orchestration.base.service'; import { OrchestrationService } from '../../orchestration.service';
import config from '@/config';
@Service() @Service()
export class OrchestrationWebhookService extends OrchestrationService { export class OrchestrationWebhookService extends OrchestrationService {
sanityCheck(): boolean { sanityCheck(): boolean {
return this.isInitialized && this.isQueueMode && this.isWebhookInstance; return (
this.isInitialized &&
config.get('executions.mode') === 'queue' &&
config.get('generic.instanceType') === 'webhook'
);
} }
} }

View file

@ -1,11 +1,16 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { AbstractEventMessage } from '@/eventbus/EventMessageClasses/AbstractEventMessage'; import type { AbstractEventMessage } from '@/eventbus/EventMessageClasses/AbstractEventMessage';
import { OrchestrationService } from '../../orchestration.base.service'; import { OrchestrationService } from '../../orchestration.service';
import config from '@/config';
@Service() @Service()
export class OrchestrationWorkerService extends OrchestrationService { export class OrchestrationWorkerService extends OrchestrationService {
sanityCheck(): boolean { sanityCheck(): boolean {
return this.isInitialized && this.isQueueMode && this.isWorkerInstance; return (
this.isInitialized &&
config.get('executions.mode') === 'queue' &&
config.get('generic.instanceType') === 'worker'
);
} }
async publishToEventLog(message: AbstractEventMessage) { async publishToEventLog(message: AbstractEventMessage) {

View file

@ -28,7 +28,7 @@ export class PruningService {
private readonly binaryDataService: BinaryDataService, private readonly binaryDataService: BinaryDataService,
) {} ) {}
isPruningEnabled() { private isPruningEnabled() {
if ( if (
!config.getEnv('executions.pruneData') || !config.getEnv('executions.pruneData') ||
inTest || inTest ||
@ -52,6 +52,8 @@ export class PruningService {
* @important Call this method only after DB migrations have completed. * @important Call this method only after DB migrations have completed.
*/ */
startPruning() { startPruning() {
if (!this.isPruningEnabled()) return;
if (this.isShuttingDown) { if (this.isShuttingDown) {
this.logger.warn('[Pruning] Cannot start pruning while shutting down'); this.logger.warn('[Pruning] Cannot start pruning while shutting down');
return; return;
@ -64,6 +66,8 @@ export class PruningService {
} }
stopPruning() { stopPruning() {
if (!this.isPruningEnabled()) return;
this.logger.debug('[Pruning] Removing soft-deletion and hard-deletion timers'); this.logger.debug('[Pruning] Removing soft-deletion and hard-deletion timers');
clearInterval(this.softDeletionInterval); clearInterval(this.softDeletionInterval);

View file

@ -22,7 +22,7 @@ import { InternalHooks } from '@/InternalHooks';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
@ -38,7 +38,7 @@ export class WorkflowService {
private readonly ownershipService: OwnershipService, private readonly ownershipService: OwnershipService,
private readonly tagService: TagService, private readonly tagService: TagService,
private readonly workflowHistoryService: WorkflowHistoryService, private readonly workflowHistoryService: WorkflowHistoryService,
private readonly multiMainSetup: MultiMainSetup, private readonly orchestrationService: OrchestrationService,
private readonly externalHooks: ExternalHooks, private readonly externalHooks: ExternalHooks,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,
) {} ) {}
@ -227,12 +227,12 @@ export class WorkflowService {
} }
} }
await this.multiMainSetup.init(); await this.orchestrationService.init();
const newState = updatedWorkflow.active; const newState = updatedWorkflow.active;
if (this.multiMainSetup.isEnabled && oldState !== newState) { if (this.orchestrationService.isMultiMainSetupEnabled && oldState !== newState) {
await this.multiMainSetup.publish('workflowActiveStateChanged', { await this.orchestrationService.publish('workflowActiveStateChanged', {
workflowId, workflowId,
oldState, oldState,
newState, newState,

View file

@ -17,7 +17,7 @@ import type { User } from '@db/entities/User';
import type { WebhookEntity } from '@db/entities/WebhookEntity'; import type { WebhookEntity } from '@db/entities/WebhookEntity';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { chooseRandomly } from './shared/random'; import { chooseRandomly } from './shared/random';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { mockInstance } from '../shared/mocking'; import { mockInstance } from '../shared/mocking';
import { setSchedulerAsLoadedNode } from './shared/utils'; import { setSchedulerAsLoadedNode } from './shared/utils';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
@ -34,8 +34,8 @@ mockInstance(ExecutionService);
mockInstance(WorkflowService); mockInstance(WorkflowService);
const webhookService = mockInstance(WebhookService); const webhookService = mockInstance(WebhookService);
const multiMainSetup = mockInstance(MultiMainSetup, { const orchestrationService = mockInstance(OrchestrationService, {
isEnabled: false, isMultiMainSetupEnabled: false,
isLeader: false, isLeader: false,
isFollower: false, isFollower: false,
}); });
@ -266,8 +266,8 @@ describe('add()', () => {
const workflow = await createWorkflow({ active: true }, owner); const workflow = await createWorkflow({ active: true }, owner);
jest.replaceProperty(multiMainSetup, 'isEnabled', true); jest.replaceProperty(orchestrationService, 'isMultiMainSetupEnabled', true);
jest.replaceProperty(multiMainSetup, 'isLeader', true); jest.replaceProperty(orchestrationService, 'isLeader', true);
const addWebhooksSpy = jest.spyOn(activeWorkflowRunner, 'addWebhooks'); const addWebhooksSpy = jest.spyOn(activeWorkflowRunner, 'addWebhooks');
const addTriggersAndPollersSpy = jest.spyOn( const addTriggersAndPollersSpy = jest.spyOn(
@ -290,8 +290,8 @@ describe('add()', () => {
test('should add triggers and pollers only', async () => { test('should add triggers and pollers only', async () => {
const mode = 'leadershipChange'; const mode = 'leadershipChange';
jest.replaceProperty(multiMainSetup, 'isEnabled', true); jest.replaceProperty(orchestrationService, 'isMultiMainSetupEnabled', true);
jest.replaceProperty(multiMainSetup, 'isLeader', true); jest.replaceProperty(orchestrationService, 'isLeader', true);
const workflow = await createWorkflow({ active: true }, owner); const workflow = await createWorkflow({ active: true }, owner);
@ -318,8 +318,8 @@ describe('add()', () => {
test('should not add webhooks, triggers or pollers', async () => { test('should not add webhooks, triggers or pollers', async () => {
const mode = chooseRandomly(NON_LEADERSHIP_CHANGE_MODES); const mode = chooseRandomly(NON_LEADERSHIP_CHANGE_MODES);
jest.replaceProperty(multiMainSetup, 'isEnabled', true); jest.replaceProperty(orchestrationService, 'isMultiMainSetupEnabled', true);
jest.replaceProperty(multiMainSetup, 'isLeader', false); jest.replaceProperty(orchestrationService, 'isLeader', false);
const workflow = await createWorkflow({ active: true }, owner); const workflow = await createWorkflow({ active: true }, owner);

View file

@ -16,7 +16,7 @@ import { PostHogClient } from '@/posthog';
import { RedisService } from '@/services/redis.service'; import { RedisService } from '@/services/redis.service';
import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service'; import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service';
import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service'; import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
@ -38,7 +38,7 @@ beforeAll(async () => {
mockInstance(RedisService); mockInstance(RedisService);
mockInstance(RedisServicePubSubPublisher); mockInstance(RedisServicePubSubPublisher);
mockInstance(RedisServicePubSubSubscriber); mockInstance(RedisServicePubSubSubscriber);
mockInstance(MultiMainSetup); mockInstance(OrchestrationService);
}); });
test('worker initializes all its components', async () => { test('worker initializes all its components', async () => {

View file

@ -7,6 +7,7 @@ import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import { setupTestServer } from './shared/utils'; import { setupTestServer } from './shared/utils';
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { createOwner } from './shared/db/users'; import { createOwner } from './shared/db/users';
import { OrchestrationService } from '@/services/orchestration.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
describe('DebugController', () => { describe('DebugController', () => {
@ -36,9 +37,9 @@ describe('DebugController', () => {
activeWorkflowRunner.allActiveInMemory.mockReturnValue([workflowId]); activeWorkflowRunner.allActiveInMemory.mockReturnValue([workflowId]);
activeWorkflowRunner.getAllWorkflowActivationErrors.mockResolvedValue(activationErrors); activeWorkflowRunner.getAllWorkflowActivationErrors.mockResolvedValue(activationErrors);
jest.spyOn(MultiMainSetup.prototype, 'instanceId', 'get').mockReturnValue(instanceId); jest.spyOn(OrchestrationService.prototype, 'instanceId', 'get').mockReturnValue(instanceId);
jest.spyOn(MultiMainSetup.prototype, 'fetchLeaderKey').mockResolvedValue(leaderKey); jest.spyOn(MultiMainSetup.prototype, 'fetchLeaderKey').mockResolvedValue(leaderKey);
jest.spyOn(MultiMainSetup.prototype, 'isLeader', 'get').mockReturnValue(true); jest.spyOn(OrchestrationService.prototype, 'isLeader', 'get').mockReturnValue(true);
const response = await ownerAgent.get('/debug/multi-main-setup').expect(200); const response = await ownerAgent.get('/debug/multi-main-setup').expect(200);

View file

@ -16,7 +16,7 @@ import { AUTH_COOKIE_NAME } from '@/constants';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { mockNodeTypesData } from '../../../unit/Helpers'; import { mockNodeTypesData } from '../../../unit/Helpers';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { mockInstance } from '../../../shared/mocking'; import { mockInstance } from '../../../shared/mocking';
import { ExecutionService } from '@/executions/execution.service'; import { ExecutionService } from '@/executions/execution.service';
@ -30,7 +30,7 @@ export { setupTestServer } from './testServer';
* Initialize node types. * Initialize node types.
*/ */
export async function initActiveWorkflowRunner() { export async function initActiveWorkflowRunner() {
mockInstance(MultiMainSetup); mockInstance(OrchestrationService);
mockInstance(ExecutionService); mockInstance(ExecutionService);
const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner'); const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner');

View file

@ -4,7 +4,7 @@ import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowService } from '@/workflows/workflow.service'; import { WorkflowService } from '@/workflows/workflow.service';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
@ -14,13 +14,13 @@ import { createWorkflow } from '../shared/db/workflows';
let workflowService: WorkflowService; let workflowService: WorkflowService;
let activeWorkflowRunner: ActiveWorkflowRunner; let activeWorkflowRunner: ActiveWorkflowRunner;
let multiMainSetup: MultiMainSetup; let orchestrationService: OrchestrationService;
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();
activeWorkflowRunner = mockInstance(ActiveWorkflowRunner); activeWorkflowRunner = mockInstance(ActiveWorkflowRunner);
multiMainSetup = mockInstance(MultiMainSetup); orchestrationService = mockInstance(OrchestrationService);
mockInstance(Telemetry); mockInstance(Telemetry);
workflowService = new WorkflowService( workflowService = new WorkflowService(
@ -33,7 +33,7 @@ beforeAll(async () => {
mock(), mock(),
mock(), mock(),
mock(), mock(),
multiMainSetup, orchestrationService,
mock(), mock(),
activeWorkflowRunner, activeWorkflowRunner,
); );
@ -89,7 +89,7 @@ describe('update()', () => {
const owner = await createOwner(); const owner = await createOwner();
const workflow = await createWorkflow({ active: true }, owner); const workflow = await createWorkflow({ active: true }, owner);
const publishSpy = jest.spyOn(multiMainSetup, 'publish'); const publishSpy = jest.spyOn(orchestrationService, 'publish');
workflow.active = false; workflow.active = false;
await workflowService.update(owner, workflow, workflow.id); await workflowService.update(owner, workflow, workflow.id);
@ -109,7 +109,7 @@ describe('update()', () => {
const owner = await createOwner(); const owner = await createOwner();
const workflow = await createWorkflow({ active: true }, owner); const workflow = await createWorkflow({ active: true }, owner);
const publishSpy = jest.spyOn(multiMainSetup, 'publish'); const publishSpy = jest.spyOn(orchestrationService, 'publish');
await workflowService.update(owner, workflow, workflow.id); await workflowService.update(owner, workflow, workflow.id);

View file

@ -6,7 +6,7 @@ import { License } from '@/License';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { mockInstance } from '../shared/mocking'; import { mockInstance } from '../shared/mocking';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { OrchestrationService } from '@/services/orchestration.service';
jest.mock('@n8n_io/license-sdk'); jest.mock('@n8n_io/license-sdk');
@ -28,7 +28,7 @@ describe('License', () => {
let license: License; let license: License;
const logger = mockInstance(Logger); const logger = mockInstance(Logger);
const instanceSettings = mockInstance(InstanceSettings, { instanceId: MOCK_INSTANCE_ID }); const instanceSettings = mockInstance(InstanceSettings, { instanceId: MOCK_INSTANCE_ID });
mockInstance(MultiMainSetup); mockInstance(OrchestrationService);
beforeEach(async () => { beforeEach(async () => {
license = new License(logger, instanceSettings, mock(), mock(), mock()); license = new License(logger, instanceSettings, mock(), mock(), mock());

View file

@ -1,6 +1,6 @@
import Container from 'typedi'; import Container from 'typedi';
import config from '@/config'; import config from '@/config';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationService } from '@/services/orchestration.service';
import type { RedisServiceWorkerResponseObject } from '@/services/redis/RedisServiceCommands'; import type { RedisServiceWorkerResponseObject } from '@/services/redis/RedisServiceCommands';
import { eventBus } from '@/eventbus'; import { eventBus } from '@/eventbus';
import { RedisService } from '@/services/redis.service'; import { RedisService } from '@/services/redis.service';
@ -14,7 +14,7 @@ import { Push } from '@/push';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
const os = Container.get(SingleMainSetup); const os = Container.get(OrchestrationService);
const handler = Container.get(OrchestrationHandlerMainService); const handler = Container.get(OrchestrationHandlerMainService);
mockInstance(ActiveWorkflowRunner); mockInstance(ActiveWorkflowRunner);