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

@@ -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> {