feat(core): Add command to trigger license refresh on workers (#7184)
This PR implements the updated license SDK so that worker and webhook instances do not auto-renew licenses any more. Instead, they receive a `reloadLicense` command via the Redis client that will fetch the updated license after it was saved on the main instance This also contains some refactoring with moving redis sub and pub clients into the event bus directly, to prevent cyclic dependency issues.
This commit is contained in:
committed by
GitHub
parent
d317e09c59
commit
9f797b96d8
@@ -1,4 +1,4 @@
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { LoggerProxy, jsonParse } from 'n8n-workflow';
|
||||
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
|
||||
import type { DeleteResult } from 'typeorm';
|
||||
import type {
|
||||
@@ -27,9 +27,18 @@ import {
|
||||
} from '../EventMessageClasses/EventMessageGeneric';
|
||||
import { recoverExecutionDataFromEventLogMessages } from './recoverEvents';
|
||||
import { METRICS_EVENT_NAME } from '../MessageEventBusDestination/Helpers.ee';
|
||||
import Container from 'typedi';
|
||||
import Container, { Service } from 'typedi';
|
||||
import { ExecutionRepository, WorkflowRepository } from '@/databases/repositories';
|
||||
import { OrchestrationService } from '../../services/orchestration.service';
|
||||
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 type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions';
|
||||
import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers';
|
||||
import { messageToRedisServiceCommandObject } from '@/services/orchestration/helpers';
|
||||
|
||||
export type EventMessageReturnMode = 'sent' | 'unsent' | 'all' | 'unfinished';
|
||||
|
||||
@@ -41,13 +50,21 @@ export interface MessageWithCallback {
|
||||
export interface MessageEventBusInitializeOptions {
|
||||
skipRecoveryPass?: boolean;
|
||||
workerId?: string;
|
||||
uniqueInstanceId?: string;
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class MessageEventBus extends EventEmitter {
|
||||
private static instance: MessageEventBus;
|
||||
|
||||
isInitialized: boolean;
|
||||
|
||||
uniqueInstanceId: string;
|
||||
|
||||
redisPublisher: RedisServicePubSubPublisher;
|
||||
|
||||
redisSubscriber: RedisServicePubSubSubscriber;
|
||||
|
||||
logWriter: MessageEventBusLogWriter;
|
||||
|
||||
destinations: {
|
||||
@@ -76,11 +93,30 @@ 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
LoggerProxy.debug('Initializing event bus...');
|
||||
|
||||
const savedEventDestinations = await Db.collections.EventDestinations.find({});
|
||||
@@ -89,7 +125,7 @@ export class MessageEventBus extends EventEmitter {
|
||||
try {
|
||||
const destination = messageEventBusDestinationFromDb(this, destinationData);
|
||||
if (destination) {
|
||||
await this.addDestination(destination);
|
||||
await this.addDestination(destination, false);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
@@ -182,10 +218,13 @@ export class MessageEventBus extends EventEmitter {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
async addDestination(destination: MessageEventBusDestination) {
|
||||
await this.removeDestination(destination.getId());
|
||||
async addDestination(destination: MessageEventBusDestination, notifyWorkers: boolean = true) {
|
||||
await this.removeDestination(destination.getId(), false);
|
||||
this.destinations[destination.getId()] = destination;
|
||||
this.destinations[destination.getId()].startListening();
|
||||
if (notifyWorkers) {
|
||||
await this.broadcastRestartEventbusAfterDestinationUpdate();
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
@@ -199,19 +238,62 @@ export class MessageEventBus extends EventEmitter {
|
||||
return result.sort((a, b) => (a.__type ?? '').localeCompare(b.__type ?? ''));
|
||||
}
|
||||
|
||||
async removeDestination(id: string): Promise<DeleteResult | undefined> {
|
||||
async removeDestination(
|
||||
id: string,
|
||||
notifyWorkers: boolean = true,
|
||||
): Promise<DeleteResult | undefined> {
|
||||
let result;
|
||||
if (Object.keys(this.destinations).includes(id)) {
|
||||
await this.destinations[id].close();
|
||||
result = await this.destinations[id].deleteFromDb();
|
||||
delete this.destinations[id];
|
||||
}
|
||||
if (notifyWorkers) {
|
||||
await this.broadcastRestartEventbusAfterDestinationUpdate();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async handleRedisEventBusMessage(messageString: string) {
|
||||
const eventData = jsonParse<AbstractEventMessageOptions>(messageString);
|
||||
if (eventData) {
|
||||
const eventMessage = getEventMessageObjectByType(eventData);
|
||||
if (eventMessage) {
|
||||
await Container.get(MessageEventBus).send(eventMessage);
|
||||
}
|
||||
}
|
||||
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 Container.get(OrchestrationService).restartEventBus();
|
||||
await this.redisPublisher.publishToCommandChannel({
|
||||
senderId: this.uniqueInstanceId,
|
||||
command: 'restartEventBus',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +317,8 @@ 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.');
|
||||
}
|
||||
@@ -417,4 +501,4 @@ export class MessageEventBus extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
export const eventBus = MessageEventBus.getInstance();
|
||||
export const eventBus = Container.get(MessageEventBus);
|
||||
|
||||
Reference in New Issue
Block a user