refactor(core): Organize Redis under scaling mode (#10864)

This commit is contained in:
Iván Ovejero
2024-09-19 09:52:48 +02:00
committed by GitHub
parent 91008b2676
commit 69c6e0790d
24 changed files with 133 additions and 212 deletions

View File

@@ -3,11 +3,11 @@ import { mock } from 'jest-mock-extended';
import config from '@/config';
import { generateNanoId } from '@/databases/utils/generators';
import type { RedisClientService } from '@/services/redis/redis-client.service';
import type {
RedisServiceCommandObject,
RedisServiceWorkerResponseObject,
} from '@/services/redis/redis-service-commands';
} from '@/scaling/redis/redis-service-commands';
import type { RedisClientService } from '@/services/redis-client.service';
import { Publisher } from '../pubsub/publisher.service';

View File

@@ -2,7 +2,7 @@ import type { Redis as SingleNodeClient } from 'ioredis';
import { mock } from 'jest-mock-extended';
import config from '@/config';
import type { RedisClientService } from '@/services/redis/redis-client.service';
import type { RedisClientService } from '@/services/redis-client.service';
import { Subscriber } from '../pubsub/subscriber.service';

View File

@@ -1,3 +1,7 @@
export const QUEUE_NAME = 'jobs';
export const JOB_TYPE_NAME = 'job';
export const COMMAND_PUBSUB_CHANNEL = 'n8n.commands';
export const WORKER_RESPONSE_PUBSUB_CHANNEL = 'n8n.worker-response';

View File

@@ -3,11 +3,11 @@ import { Service } from 'typedi';
import config from '@/config';
import { Logger } from '@/logger';
import { RedisClientService } from '@/services/redis/redis-client.service';
import type {
RedisServiceCommandObject,
RedisServiceWorkerResponseObject,
} from '@/services/redis/redis-service-commands';
} from '@/scaling/redis/redis-service-commands';
import { RedisClientService } from '@/services/redis-client.service';
/**
* Responsible for publishing messages into the pubsub channels used by scaling mode.

View File

@@ -1,7 +1,8 @@
import type {
COMMAND_REDIS_CHANNEL,
WORKER_RESPONSE_REDIS_CHANNEL,
} from '@/services/redis/redis-constants';
import type { PushType, WorkerStatus } from '@n8n/api-types';
import type { IWorkflowDb } from '@/interfaces';
import type { COMMAND_PUBSUB_CHANNEL, WORKER_RESPONSE_PUBSUB_CHANNEL } from '../constants';
/**
* Pubsub channel used by scaling mode:
@@ -10,5 +11,90 @@ import type {
* - `n8n.worker-response` for messages sent by workers in response to commands from main processes
*/
export type ScalingPubSubChannel =
| typeof COMMAND_REDIS_CHANNEL
| typeof WORKER_RESPONSE_REDIS_CHANNEL;
| typeof COMMAND_PUBSUB_CHANNEL
| typeof WORKER_RESPONSE_PUBSUB_CHANNEL;
export type PubSubMessageMap = {
// #region Lifecycle
'reload-license': never;
'restart-event-bus': {
result: 'success' | 'error';
error?: string;
};
'reload-external-secrets-providers': {
result: 'success' | 'error';
error?: string;
};
'stop-worker': never;
// #endregion
// #region Community packages
'community-package-install': {
packageName: string;
packageVersion: string;
};
'community-package-update': {
packageName: string;
packageVersion: string;
};
'community-package-uninstall': {
packageName: string;
packageVersion: string;
};
// #endregion
// #region Worker view
'get-worker-id': never;
'get-worker-status': WorkerStatus;
// #endregion
// #region Multi-main setup
'add-webhooks-triggers-and-pollers': {
workflowId: string;
};
'remove-triggers-and-pollers': {
workflowId: string;
};
'display-workflow-activation': {
workflowId: string;
};
'display-workflow-deactivation': {
workflowId: string;
};
// currently 'workflow-failed-to-activate'
'display-workflow-activation-error': {
workflowId: string;
errorMessage: string;
};
'relay-execution-lifecycle-event': {
type: PushType;
args: Record<string, unknown>;
pushRef: string;
};
'clear-test-webhooks': {
webhookKey: string;
workflowEntity: IWorkflowDb;
pushRef: string;
};
// #endregion
};

View File

@@ -3,7 +3,7 @@ import { Service } from 'typedi';
import config from '@/config';
import { Logger } from '@/logger';
import { RedisClientService } from '@/services/redis/redis-client.service';
import { RedisClientService } from '@/services/redis-client.service';
import type { ScalingPubSubChannel } from './pubsub.types';

View File

@@ -0,0 +1,103 @@
import type { PushType, WorkerStatus } from '@n8n/api-types';
import type { IWorkflowDb } from '@/interfaces';
export type RedisServiceCommand =
| 'getStatus'
| 'getId'
| 'restartEventBus'
| 'stopWorker'
| 'reloadLicense'
| 'reloadExternalSecretsProviders'
| 'community-package-install'
| 'community-package-update'
| 'community-package-uninstall'
| 'display-workflow-activation' // multi-main only
| 'display-workflow-deactivation' // multi-main only
| 'add-webhooks-triggers-and-pollers' // multi-main only
| 'remove-triggers-and-pollers' // multi-main only
| 'workflow-failed-to-activate' // multi-main only
| 'relay-execution-lifecycle-event' // multi-main only
| 'clear-test-webhooks'; // multi-main only
/**
* An object to be sent via Redis pubsub from the main process to the workers.
* @field command: The command to be executed.
* @field targets: The targets to execute the command on. Leave empty to execute on all workers or specify worker ids.
* @field payload: Optional arguments to be sent with the command.
*/
export type RedisServiceBaseCommand =
| {
senderId: string;
command: Exclude<
RedisServiceCommand,
| 'relay-execution-lifecycle-event'
| 'clear-test-webhooks'
| 'community-package-install'
| 'community-package-update'
| 'community-package-uninstall'
>;
payload?: {
[key: string]: string | number | boolean | string[] | number[] | boolean[];
};
}
| {
senderId: string;
command: 'relay-execution-lifecycle-event';
payload: { type: PushType; args: Record<string, unknown>; pushRef: string };
}
| {
senderId: string;
command: 'clear-test-webhooks';
payload: { webhookKey: string; workflowEntity: IWorkflowDb; pushRef: string };
}
| {
senderId: string;
command:
| 'community-package-install'
| 'community-package-update'
| 'community-package-uninstall';
payload: { packageName: string; packageVersion: string };
};
export type RedisServiceWorkerResponseObject = {
workerId: string;
} & (
| RedisServiceBaseCommand
| {
command: 'getStatus';
payload: WorkerStatus;
}
| {
command: 'getId';
}
| {
command: 'restartEventBus';
payload: {
result: 'success' | 'error';
error?: string;
};
}
| {
command: 'reloadExternalSecretsProviders';
payload: {
result: 'success' | 'error';
error?: string;
};
}
| {
command: 'stopWorker';
}
| {
command: 'workflowActiveStateChanged';
payload: {
oldState: boolean;
newState: boolean;
workflowId: string;
};
}
) & { targets?: string[] };
export type RedisServiceCommandObject = {
targets?: string[];
} & RedisServiceBaseCommand;

View File

@@ -0,0 +1,19 @@
export type RedisClientType = N8nRedisClientType | BullRedisClientType;
/**
* Redis client used by n8n.
*
* - `subscriber(n8n)` to listen for messages from scaling mode communication channels
* - `publisher(n8n)` to send messages into scaling mode communication channels
* - `cache(n8n)` for caching operations (variables, resource ownership, etc.)
*/
type N8nRedisClientType = 'subscriber(n8n)' | 'publisher(n8n)' | 'cache(n8n)';
/**
* Redis client used internally by Bull. Suffixed with `(bull)` at `ScalingService.setupQueue`.
*
* - `subscriber(bull)` for event listening
* - `client(bull)` for general queue operations
* - `bclient(bull)` for blocking operations when processing jobs
*/
type BullRedisClientType = 'subscriber(bull)' | 'client(bull)' | 'bclient(bull)';

View File

@@ -24,7 +24,7 @@ import type {
JobStatus,
JobId,
QueueRecoveryContext,
PubSubMessage,
JobReport,
} from './scaling.types';
@Service()
@@ -46,7 +46,7 @@ export class ScalingService {
async setupQueue() {
const { default: BullQueue } = await import('bull');
const { RedisClientService } = await import('@/services/redis/redis-client.service');
const { RedisClientService } = await import('@/services/redis-client.service');
const service = Container.get(RedisClientService);
const bullPrefix = this.globalConfig.queue.bull.prefix;
@@ -265,7 +265,7 @@ export class ScalingService {
}
}
private isPubSubMessage(candidate: unknown): candidate is PubSubMessage {
private isPubSubMessage(candidate: unknown): candidate is JobReport {
return typeof candidate === 'object' && candidate !== null && 'kind' in candidate;
}

View File

@@ -23,11 +23,11 @@ export type JobStatus = Bull.JobStatus;
export type JobOptions = Bull.JobOptions;
export type PubSubMessage = MessageToMain | MessageToWorker;
export type JobReport = JobReportToMain | JobReportToWorker;
type MessageToMain = RespondToWebhookMessage;
type JobReportToMain = RespondToWebhookMessage;
type MessageToWorker = AbortJobMessage;
type JobReportToWorker = AbortJobMessage;
type RespondToWebhookMessage = {
kind: 'respond-to-webhook';