fix(core): Make senderId required for all command messages (#7252)

all commands sent between main instance and workers need to contain a
server id to prevent senders from reacting to their own messages,
causing loops

this PR makes sure all sent messages contain a sender id by default as
part of constructing a sending redis client.

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Michael Auerswald
2023-09-26 13:58:06 +02:00
committed by GitHub
parent 77d6e3fc07
commit 4b014286cf
23 changed files with 231 additions and 203 deletions

View File

@@ -102,6 +102,7 @@
"@n8n/client-oauth2": "workspace:*",
"@n8n_io/license-sdk": "~2.6.0",
"@oclif/command": "^1.8.16",
"@oclif/config": "^1.18.17",
"@oclif/core": "^1.16.4",
"@oclif/errors": "^1.3.6",
"@rudderstack/rudder-sdk-node": "1.0.6",

View File

@@ -117,7 +117,7 @@ export abstract class AbstractServer {
if (config.getEnv('executions.mode') === 'queue') {
// will start the redis connections
await Container.get(OrchestrationService).init(this.uniqueInstanceId);
await Container.get(OrchestrationService).init();
}
}

View File

@@ -1,4 +1,4 @@
import type { TEntitlement, TLicenseBlock } from '@n8n_io/license-sdk';
import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk';
import { LicenseManager } from '@n8n_io/license-sdk';
import type { ILogger } from 'n8n-workflow';
import { getLogger } from './Logger';
@@ -50,6 +50,9 @@ export class License {
const saveCertStr = isMainInstance
? async (value: TLicenseBlock) => this.saveCertStr(value)
: async () => {};
const onFeatureChange = isMainInstance
? async (features: TFeatures) => this.onFeatureChange(features)
: async () => {};
try {
this.manager = new LicenseManager({
@@ -64,6 +67,7 @@ export class License {
loadCertStr: async () => this.loadCertStr(),
saveCertStr,
deviceFingerprint: () => instanceId,
onFeatureChange,
});
await this.manager.initialize();
@@ -89,6 +93,18 @@ export class License {
return databaseSettings?.value ?? '';
}
async onFeatureChange(_features: TFeatures): Promise<void> {
if (config.getEnv('executions.mode') === 'queue') {
if (!this.redisPublisher) {
this.logger.debug('Initializing Redis publisher for License Service');
this.redisPublisher = await Container.get(RedisService).getPubSubPublisher();
}
await this.redisPublisher.publishToCommandChannel({
command: 'reloadLicense',
});
}
}
async saveCertStr(value: TLicenseBlock): Promise<void> {
// if we have an ephemeral license, we don't want to save it to the database
if (config.get('license.cert')) return;
@@ -100,15 +116,6 @@ export class License {
},
['key'],
);
if (config.getEnv('executions.mode') === 'queue') {
if (!this.redisPublisher) {
this.logger.debug('Initializing Redis publisher for License Service');
this.redisPublisher = await Container.get(RedisService).getPubSubPublisher();
}
await this.redisPublisher.publishToCommandChannel({
command: 'reloadLicense',
});
}
}
async activate(activationKey: string): Promise<void> {

View File

@@ -1474,9 +1474,7 @@ export class Server extends AbstractServer {
// ----------------------------------------
if (!eventBus.isInitialized) {
await eventBus.initialize({
uniqueInstanceId: this.uniqueInstanceId,
});
await eventBus.initialize();
}
if (this.endpointPresetCredentials !== '') {

View File

@@ -22,6 +22,7 @@ import { PostHogClient } from '@/posthog';
import { License } from '@/License';
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
import { initExpressionEvaluator } from '@/ExpressionEvalator';
import { generateHostInstanceId } from '../databases/utils/generators';
export abstract class BaseCommand extends Command {
protected logger = LoggerProxy.init(getLogger());
@@ -36,6 +37,10 @@ export abstract class BaseCommand extends Command {
protected instanceId: string;
instanceType: N8nInstanceType = 'main';
queueModeId: string;
protected server?: AbstractServer;
async init(): Promise<void> {
@@ -83,6 +88,22 @@ export abstract class BaseCommand extends Command {
await Container.get(InternalHooks).init(this.instanceId);
}
protected setInstanceType(instanceType: N8nInstanceType) {
this.instanceType = instanceType;
config.set('generic.instanceType', instanceType);
}
protected setInstanceQueueModeId() {
if (config.getEnv('executions.mode') === 'queue') {
if (config.get('redis.queueModeId')) {
this.queueModeId = config.get('redis.queueModeId');
return;
}
this.queueModeId = generateHostInstanceId(this.instanceType);
config.set('redis.queueModeId', this.queueModeId);
}
}
protected async stopProcess() {
// This needs to be overridden
}
@@ -115,11 +136,9 @@ export abstract class BaseCommand extends Command {
await this.externalHooks.init();
}
async initLicense(instanceType: N8nInstanceType = 'main'): Promise<void> {
config.set('generic.instanceType', instanceType);
async initLicense(): Promise<void> {
const license = Container.get(License);
await license.init(this.instanceId, instanceType);
await license.init(this.instanceId, this.instanceType ?? 'main');
const activationKey = config.getEnv('license.activationKey');

View File

@@ -30,6 +30,7 @@ import { BaseCommand } from './BaseCommand';
import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License';
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
import { IConfig } from '@oclif/config';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const open = require('open');
@@ -65,6 +66,12 @@ export class Start extends BaseCommand {
protected server = new Server();
constructor(argv: string[], cmdConfig: IConfig) {
super(argv, cmdConfig);
this.setInstanceType('main');
this.setInstanceQueueModeId();
}
/**
* Opens the UI in browser
*/
@@ -196,11 +203,16 @@ export class Start extends BaseCommand {
async init() {
await this.initCrashJournal();
await super.init();
this.logger.info('Initializing n8n process');
if (config.getEnv('executions.mode') === 'queue') {
this.logger.debug('Main Instance running in queue mode');
this.logger.debug(`Queue mode id: ${this.queueModeId}`);
}
await super.init();
this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
await this.initLicense('main');
await this.initLicense();
await this.initBinaryDataService();
await this.initExternalHooks();
await this.initExternalSecrets();

View File

@@ -6,6 +6,7 @@ import { WebhookServer } from '@/WebhookServer';
import { Queue } from '@/Queue';
import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
import { IConfig } from '@oclif/config';
export class Webhook extends BaseCommand {
static description = 'Starts n8n webhook process. Intercepts only production URLs.';
@@ -18,6 +19,15 @@ export class Webhook extends BaseCommand {
protected server = new WebhookServer();
constructor(argv: string[], cmdConfig: IConfig) {
super(argv, cmdConfig);
this.setInstanceType('webhook');
if (this.queueModeId) {
this.logger.debug(`Webhook Instance queue mode id: ${this.queueModeId}`);
}
this.setInstanceQueueModeId();
}
/**
* Stops n8n in a graceful way.
* Make for example sure that all the webhooks from third party services
@@ -75,9 +85,13 @@ export class Webhook extends BaseCommand {
}
await this.initCrashJournal();
this.logger.info('Initializing n8n webhook process');
this.logger.debug(`Queue mode id: ${this.queueModeId}`);
await super.init();
await this.initLicense('webhook');
await this.initLicense();
await this.initBinaryDataService();
await this.initExternalHooks();
await this.initExternalSecrets();

View File

@@ -29,7 +29,6 @@ import { N8N_VERSION } from '@/constants';
import { BaseCommand } from './BaseCommand';
import { ExecutionRepository } from '@db/repositories';
import { OwnershipService } from '@/services/ownership.service';
import { generateHostInstanceId } from '@/databases/utils/generators';
import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { rawBodyReader, bodyParser } from '@/middlewares';
@@ -38,6 +37,7 @@ import { RedisServicePubSubPublisher } from '../services/redis/RedisServicePubSu
import { RedisServicePubSubSubscriber } from '../services/redis/RedisServicePubSubSubscriber';
import { EventMessageGeneric } from '../eventbus/EventMessageClasses/EventMessageGeneric';
import { getWorkerCommandReceivedHandler } from '../worker/workerCommandHandler';
import { IConfig } from '@oclif/config';
export class Worker extends BaseCommand {
static description = '\nStarts a n8n worker';
@@ -58,8 +58,6 @@ export class Worker extends BaseCommand {
static jobQueue: JobQueue;
readonly uniqueInstanceId = generateHostInstanceId('worker');
redisPublisher: RedisServicePubSubPublisher;
redisSubscriber: RedisServicePubSubSubscriber;
@@ -250,13 +248,22 @@ export class Worker extends BaseCommand {
};
}
constructor(argv: string[], cmdConfig: IConfig) {
super(argv, cmdConfig);
this.setInstanceType('worker');
this.setInstanceQueueModeId();
}
async init() {
await this.initCrashJournal();
await super.init();
this.logger.debug(`Worker ID: ${this.uniqueInstanceId}`);
this.logger.debug('Starting n8n worker...');
await this.initLicense('worker');
this.logger.debug('Starting n8n worker...');
this.logger.debug(`Queue mode id: ${this.queueModeId}`);
await super.init();
await this.initLicense();
await this.initBinaryDataService();
await this.initExternalHooks();
await this.initExternalSecrets();
@@ -267,8 +274,7 @@ export class Worker extends BaseCommand {
async initEventBus() {
await eventBus.initialize({
workerId: this.uniqueInstanceId,
uniqueInstanceId: this.uniqueInstanceId,
workerId: this.queueModeId,
});
}
@@ -286,7 +292,7 @@ export class Worker extends BaseCommand {
new EventMessageGeneric({
eventName: 'n8n.worker.started',
payload: {
workerId: this.uniqueInstanceId,
workerId: this.queueModeId,
},
}),
);
@@ -295,7 +301,7 @@ export class Worker extends BaseCommand {
'WorkerCommandReceivedHandler',
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkerCommandReceivedHandler({
uniqueInstanceId: this.uniqueInstanceId,
queueModeId: this.queueModeId,
instanceId: this.instanceId,
redisPublisher: this.redisPublisher,
getRunningJobIds: () => Object.keys(Worker.runningJobs),

View File

@@ -1138,6 +1138,11 @@ export const schema = {
default: 'n8n',
env: 'N8N_REDIS_KEY_PREFIX',
},
queueModeId: {
doc: 'Unique ID for this n8n instance, is usually set automatically by n8n during startup',
format: String,
default: '',
},
},
cache: {

View File

@@ -32,13 +32,9 @@ import { ExecutionRepository, WorkflowRepository } from '@/databases/repositorie
import { RedisService } from '@/services/redis.service';
import type { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher';
import type { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber';
import {
COMMAND_REDIS_CHANNEL,
EVENT_BUS_REDIS_CHANNEL,
} from '@/services/redis/RedisServiceHelper';
import { EVENT_BUS_REDIS_CHANNEL } from '@/services/redis/RedisServiceHelper';
import type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions';
import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers';
import { messageToRedisServiceCommandObject } from '@/services/orchestration/helpers';
export type EventMessageReturnMode = 'sent' | 'unsent' | 'all' | 'unfinished';
@@ -50,7 +46,6 @@ export interface MessageWithCallback {
export interface MessageEventBusInitializeOptions {
skipRecoveryPass?: boolean;
workerId?: string;
uniqueInstanceId?: string;
}
@Service()
@@ -59,8 +54,6 @@ export class MessageEventBus extends EventEmitter {
isInitialized: boolean;
uniqueInstanceId: string;
redisPublisher: RedisServicePubSubPublisher;
redisSubscriber: RedisServicePubSubSubscriber;
@@ -93,25 +86,20 @@ export class MessageEventBus extends EventEmitter {
*
* Sets `isInitialized` to `true` once finished.
*/
async initialize(options: MessageEventBusInitializeOptions): Promise<void> {
async initialize(options?: MessageEventBusInitializeOptions): Promise<void> {
if (this.isInitialized) {
return;
}
this.uniqueInstanceId = options?.uniqueInstanceId ?? '';
if (config.getEnv('executions.mode') === 'queue') {
this.redisPublisher = await Container.get(RedisService).getPubSubPublisher();
this.redisSubscriber = await Container.get(RedisService).getPubSubSubscriber();
await this.redisSubscriber.subscribeToEventLog();
await this.redisSubscriber.subscribeToCommandChannel();
this.redisSubscriber.addMessageHandler(
'MessageEventBusMessageReceiver',
async (channel: string, messageString: string) => {
if (channel === EVENT_BUS_REDIS_CHANNEL) {
await this.handleRedisEventBusMessage(messageString);
} else if (channel === COMMAND_REDIS_CHANNEL) {
await this.handleRedisCommandMessage(messageString);
}
},
);
@@ -265,33 +253,9 @@ export class MessageEventBus extends EventEmitter {
return eventData;
}
async handleRedisCommandMessage(messageString: string) {
const message = messageToRedisServiceCommandObject(messageString);
if (message) {
if (
message.senderId === this.uniqueInstanceId ||
(message.targets && !message.targets.includes(this.uniqueInstanceId))
) {
LoggerProxy.debug(
`Skipping command message ${message.command} because it's not for this instance.`,
);
return message;
}
switch (message.command) {
case 'restartEventBus':
await this.restart();
default:
break;
}
return message;
}
return;
}
async broadcastRestartEventbusAfterDestinationUpdate() {
if (config.getEnv('executions.mode') === 'queue') {
await this.redisPublisher.publishToCommandChannel({
senderId: this.uniqueInstanceId,
command: 'restartEventBus',
});
}
@@ -317,7 +281,6 @@ export class MessageEventBus extends EventEmitter {
);
await this.destinations[destinationName].close();
}
await this.redisSubscriber?.unSubscribeFromCommandChannel();
await this.redisSubscriber?.unSubscribeFromEventLog();
this.isInitialized = false;
LoggerProxy.debug('EventBus shut down.');

View File

@@ -18,7 +18,7 @@ import {
isEventMessageConfirm,
} from '../EventMessageClasses/EventMessageConfirm';
import { once as eventOnce } from 'events';
import { inTest } from '../../constants';
import { inTest } from '@/constants';
interface MessageEventBusLogWriterConstructorOptions {
logBaseName?: string;

View File

@@ -10,20 +10,13 @@ import { handleCommandMessage } from './orchestration/handleCommandMessage';
export class OrchestrationService {
private initialized = false;
private _uniqueInstanceId = '';
get uniqueInstanceId(): string {
return this._uniqueInstanceId;
}
redisPublisher: RedisServicePubSubPublisher;
redisSubscriber: RedisServicePubSubSubscriber;
constructor(readonly redisService: RedisService) {}
async init(uniqueInstanceId: string) {
this._uniqueInstanceId = uniqueInstanceId;
async init() {
await this.initPublisher();
await this.initSubscriber();
this.initialized = true;
@@ -50,7 +43,7 @@ export class OrchestrationService {
if (channel === WORKER_RESPONSE_REDIS_CHANNEL) {
await handleWorkerResponseMessage(messageString);
} else if (channel === COMMAND_REDIS_CHANNEL) {
await handleCommandMessage(messageString, this.uniqueInstanceId);
await handleCommandMessage(messageString);
}
},
);
@@ -61,7 +54,6 @@ export class OrchestrationService {
throw new Error('OrchestrationService not initialized');
}
await this.redisPublisher.publishToCommandChannel({
senderId: this.uniqueInstanceId,
command: 'getStatus',
targets: id ? [id] : undefined,
});
@@ -72,32 +64,7 @@ export class OrchestrationService {
throw new Error('OrchestrationService not initialized');
}
await this.redisPublisher.publishToCommandChannel({
senderId: this.uniqueInstanceId,
command: 'getId',
});
}
// TODO: not implemented yet on worker side
async stopWorker(id?: string) {
if (!this.initialized) {
throw new Error('OrchestrationService not initialized');
}
await this.redisPublisher.publishToCommandChannel({
senderId: this.uniqueInstanceId,
command: 'stopWorker',
targets: id ? [id] : undefined,
});
}
// reload the license on workers after it was changed on the main instance
async reloadLicense(id?: string) {
if (!this.initialized) {
throw new Error('OrchestrationService not initialized');
}
await this.redisPublisher.publishToCommandChannel({
senderId: this.uniqueInstanceId,
command: 'reloadLicense',
targets: id ? [id] : undefined,
});
}
}

View File

@@ -1,16 +1,19 @@
import { LoggerProxy } from 'n8n-workflow';
import { messageToRedisServiceCommandObject } from './helpers';
import config from '@/config';
import { MessageEventBus } from '../../eventbus/MessageEventBus/MessageEventBus';
import Container from 'typedi';
import { License } from '@/License';
// this function handles commands sent to the MAIN instance. the workers handle their own commands
export async function handleCommandMessage(messageString: string, uniqueInstanceId: string) {
export async function handleCommandMessage(messageString: string) {
const queueModeId = config.get('redis.queueModeId');
const message = messageToRedisServiceCommandObject(messageString);
if (message) {
if (
message.senderId === uniqueInstanceId ||
(message.targets && !message.targets.includes(uniqueInstanceId))
message.senderId === queueModeId ||
(message.targets && !message.targets.includes(queueModeId))
) {
// Skipping command message because it's not for this instance
LoggerProxy.debug(
`Skipping command message ${message.command} because it's not for this instance.`,
);
@@ -18,8 +21,16 @@ export async function handleCommandMessage(messageString: string, uniqueInstance
}
switch (message.command) {
case 'reloadLicense':
await Container.get(License).reload();
// at this point in time, only a single main instance is supported, thus this
// command _should_ never be caught currently (which is why we log a warning)
LoggerProxy.warn(
'Received command to reload license via Redis, but this should not have happened and is not supported on the main instance yet.',
);
// once multiple main instances are supported, this command should be handled
// await Container.get(License).reload();
break;
case 'restartEventBus':
await Container.get(MessageEventBus).restart();
default:
break;
}

View File

@@ -2,6 +2,7 @@ import type Redis from 'ioredis';
import type { Cluster } from 'ioredis';
import { getDefaultRedisClient } from './RedisServiceHelper';
import { LoggerProxy } from 'n8n-workflow';
import config from '@/config';
export type RedisClientType =
| 'subscriber'
@@ -57,8 +58,9 @@ class RedisServiceBase {
export abstract class RedisServiceBaseSender extends RedisServiceBase {
senderId: string;
setSenderId(senderId?: string): void {
this.senderId = senderId ?? '';
async init(type: RedisClientType = 'client'): Promise<void> {
await super.init(type);
this.senderId = config.get('redis.queueModeId');
}
}

View File

@@ -12,7 +12,7 @@ export type RedisServiceCommand =
* @field payload: Optional arguments to be sent with the command.
*/
type RedisServiceBaseCommand = {
senderId?: string;
senderId: string;
command: RedisServiceCommand;
payload?: {
[key: string]: string | number | boolean | string[] | number[] | boolean[];

View File

@@ -5,9 +5,8 @@ import { RedisServiceBaseSender } from './RedisServiceBaseClasses';
@Service()
export class RedisServiceListSender extends RedisServiceBaseSender {
async init(senderId?: string): Promise<void> {
async init(): Promise<void> {
await super.init('list-sender');
this.setSenderId(senderId);
}
async prepend(list: string, message: string): Promise<void> {

View File

@@ -13,9 +13,8 @@ import { RedisServiceBaseSender } from './RedisServiceBaseClasses';
@Service()
export class RedisServicePubSubPublisher extends RedisServiceBaseSender {
async init(senderId?: string): Promise<void> {
async init(): Promise<void> {
await super.init('publisher');
this.setSenderId(senderId);
}
async publish(channel: string, message: string): Promise<void> {
@@ -29,8 +28,12 @@ export class RedisServicePubSubPublisher extends RedisServiceBaseSender {
await this.publish(EVENT_BUS_REDIS_CHANNEL, message.toString());
}
async publishToCommandChannel(message: RedisServiceCommandObject): Promise<void> {
await this.publish(COMMAND_REDIS_CHANNEL, JSON.stringify(message));
async publishToCommandChannel(
message: Omit<RedisServiceCommandObject, 'senderId'>,
): Promise<void> {
const messageWithSenderId = message as RedisServiceCommandObject;
messageWithSenderId.senderId = this.senderId;
await this.publish(COMMAND_REDIS_CHANNEL, JSON.stringify(messageWithSenderId));
}
async publishToWorkerChannel(message: RedisServiceWorkerResponseObject): Promise<void> {

View File

@@ -14,9 +14,8 @@ import { RedisServiceBaseSender } from './RedisServiceBaseClasses';
@Service()
export class RedisServiceStreamProducer extends RedisServiceBaseSender {
async init(senderId?: string): Promise<void> {
async init(): Promise<void> {
await super.init('producer');
this.setSenderId(senderId);
}
async add(streamName: string, values: RedisValue[]): Promise<void> {

View File

@@ -5,9 +5,10 @@ import type { RedisServicePubSubPublisher } from '@/services/redis/RedisServiceP
import * as os from 'os';
import Container from 'typedi';
import { License } from '@/License';
import { MessageEventBus } from '../eventbus/MessageEventBus/MessageEventBus';
export function getWorkerCommandReceivedHandler(options: {
uniqueInstanceId: string;
queueModeId: string;
instanceId: string;
redisPublisher: RedisServicePubSubPublisher;
getRunningJobIds: () => string[];
@@ -25,16 +26,16 @@ export function getWorkerCommandReceivedHandler(options: {
return;
}
if (message) {
if (message.targets && !message.targets.includes(options.uniqueInstanceId)) {
if (message.targets && !message.targets.includes(options.queueModeId)) {
return; // early return if the message is not for this worker
}
switch (message.command) {
case 'getStatus':
await options.redisPublisher.publishToWorkerChannel({
workerId: options.uniqueInstanceId,
workerId: options.queueModeId,
command: message.command,
payload: {
workerId: options.uniqueInstanceId,
workerId: options.queueModeId,
runningJobs: options.getRunningJobIds(),
freeMem: os.freemem(),
totalMem: os.totalmem(),
@@ -53,13 +54,14 @@ export function getWorkerCommandReceivedHandler(options: {
break;
case 'getId':
await options.redisPublisher.publishToWorkerChannel({
workerId: options.uniqueInstanceId,
workerId: options.queueModeId,
command: message.command,
});
break;
case 'restartEventBus':
await Container.get(MessageEventBus).restart();
await options.redisPublisher.publishToWorkerChannel({
workerId: options.uniqueInstanceId,
workerId: options.queueModeId,
command: message.command,
payload: {
result: 'success',

View File

@@ -1,6 +1,7 @@
import { mockInstance } from '../shared/utils/';
import { Worker } from '@/commands/worker';
import * as Config from '@oclif/config';
import config from '@/config';
import { LoggerProxy } from 'n8n-workflow';
import { Telemetry } from '@/telemetry';
import { getLogger } from '@/Logger';
@@ -17,10 +18,11 @@ import { InternalHooks } from '@/InternalHooks';
import { PostHogClient } from '@/posthog';
import { RedisService } from '@/services/redis.service';
const config: Config.IConfig = new Config.Config({ root: __dirname });
const oclifConfig: Config.IConfig = new Config.Config({ root: __dirname });
beforeAll(async () => {
LoggerProxy.init(getLogger());
config.set('executions.mode', 'queue');
mockInstance(Telemetry);
mockInstance(PostHogClient);
mockInstance(InternalHooks);
@@ -37,7 +39,7 @@ beforeAll(async () => {
});
test('worker initializes all its components', async () => {
const worker = new Worker([], config);
const worker = new Worker([], oclifConfig);
jest.spyOn(worker, 'init');
jest.spyOn(worker, 'initLicense').mockImplementation(async () => {});
@@ -60,9 +62,9 @@ test('worker initializes all its components', async () => {
await worker.init();
expect(worker.uniqueInstanceId).toBeDefined();
expect(worker.uniqueInstanceId).toContain('worker');
expect(worker.uniqueInstanceId.length).toBeGreaterThan(15);
expect(worker.queueModeId).toBeDefined();
expect(worker.queueModeId).toContain('worker');
expect(worker.queueModeId.length).toBeGreaterThan(15);
expect(worker.initLicense).toHaveBeenCalled();
expect(worker.initBinaryDataService).toHaveBeenCalled();
expect(worker.initExternalHooks).toHaveBeenCalled();

View File

@@ -38,6 +38,7 @@ describe('License', () => {
logger: expect.anything(),
loadCertStr: expect.any(Function),
saveCertStr: expect.any(Function),
onFeatureChange: expect.any(Function),
server: MOCK_SERVER_URL,
tenantId: 1,
});
@@ -56,6 +57,7 @@ describe('License', () => {
logger: expect.anything(),
loadCertStr: expect.any(Function),
saveCertStr: expect.any(Function),
onFeatureChange: expect.any(Function),
server: MOCK_SERVER_URL,
tenantId: 1,
});

View File

@@ -4,16 +4,16 @@ import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger';
import { OrchestrationService } from '@/services/orchestration.service';
import type { RedisServiceWorkerResponseObject } from '@/services/redis/RedisServiceCommands';
import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow';
import { eventBus } from '@/eventbus';
import { RedisService } from '@/services/redis.service';
import { mockInstance } from '../../integration/shared/utils';
import { handleWorkerResponseMessage } from '../../../src/services/orchestration/handleWorkerResponseMessage';
import { handleCommandMessage } from '../../../src/services/orchestration/handleCommandMessage';
import { License } from '../../../src/License';
const os = Container.get(OrchestrationService);
let queueModeId: string;
function setDefaultConfig() {
config.set('executions.mode', 'queue');
}
@@ -27,15 +27,6 @@ const workerRestartEventbusResponse: RedisServiceWorkerResponseObject = {
},
};
const eventBusMessage = new EventMessageWorkflow({
eventName: 'n8n.workflow.success',
id: 'test',
message: 'test',
payload: {
test: 'test',
},
});
describe('Orchestration Service', () => {
beforeAll(async () => {
mockInstance(RedisService);
@@ -74,6 +65,7 @@ describe('Orchestration Service', () => {
});
});
setDefaultConfig();
queueModeId = config.get('redis.queueModeId');
});
afterAll(async () => {
@@ -83,10 +75,10 @@ describe('Orchestration Service', () => {
});
test('should initialize', async () => {
await os.init('test-orchestration-service');
await os.init();
expect(os.redisPublisher).toBeDefined();
expect(os.redisSubscriber).toBeDefined();
expect(os.uniqueInstanceId).toBeDefined();
expect(queueModeId).toBeDefined();
});
test('should handle worker responses', async () => {
@@ -97,32 +89,28 @@ describe('Orchestration Service', () => {
});
test('should handle command messages from others', async () => {
const license = Container.get(License);
license.instanceId = 'test';
jest.spyOn(license, 'reload');
jest.spyOn(LoggerProxy, 'warn');
const responseFalseId = await handleCommandMessage(
JSON.stringify({
senderId: 'test',
command: 'reloadLicense',
}),
os.uniqueInstanceId,
);
expect(responseFalseId).toBeDefined();
expect(responseFalseId!.command).toEqual('reloadLicense');
expect(responseFalseId!.senderId).toEqual('test');
expect(license.reload).toHaveBeenCalled();
jest.spyOn(license, 'reload').mockRestore();
expect(LoggerProxy.warn).toHaveBeenCalled();
jest.spyOn(LoggerProxy, 'warn').mockRestore();
});
test('should reject command messages from iteslf', async () => {
jest.spyOn(eventBus, 'restart');
const response = await handleCommandMessage(
JSON.stringify({ ...workerRestartEventbusResponse, senderId: os.uniqueInstanceId }),
os.uniqueInstanceId,
JSON.stringify({ ...workerRestartEventbusResponse, senderId: queueModeId }),
);
expect(response).toBeDefined();
expect(response!.command).toEqual('restartEventBus');
expect(response!.senderId).toEqual(os.uniqueInstanceId);
expect(response!.senderId).toEqual(queueModeId);
expect(eventBus.restart).not.toHaveBeenCalled();
jest.spyOn(eventBus, 'restart').mockRestore();
});