1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-12-27 14:13:33 +03:00

feat: Make runner concurrency configurable (no-changelog) (#11448)

This commit is contained in:
Tomi Turtiainen 2024-10-29 21:08:50 +02:00 committed by GitHub
parent ea47b025fb
commit d7ba206b30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 107 additions and 78 deletions

View File

@ -10,6 +10,7 @@
"N8N_RUNNERS_GRANT_TOKEN", "N8N_RUNNERS_GRANT_TOKEN",
"N8N_RUNNERS_N8N_URI", "N8N_RUNNERS_N8N_URI",
"N8N_RUNNERS_MAX_PAYLOAD", "N8N_RUNNERS_MAX_PAYLOAD",
"N8N_RUNNERS_MAX_CONCURRENCY",
"NODE_FUNCTION_ALLOW_BUILTIN", "NODE_FUNCTION_ALLOW_BUILTIN",
"NODE_FUNCTION_ALLOW_EXTERNAL", "NODE_FUNCTION_ALLOW_EXTERNAL",
"NODE_OPTIONS" "NODE_OPTIONS"

View File

@ -46,4 +46,8 @@ export class TaskRunnersConfig {
/** The --max-old-space-size option to use for the runner (in MB). Default means node.js will determine it based on the available memory. */ /** The --max-old-space-size option to use for the runner (in MB). Default means node.js will determine it based on the available memory. */
@Env('N8N_RUNNERS_MAX_OLD_SPACE_SIZE') @Env('N8N_RUNNERS_MAX_OLD_SPACE_SIZE')
maxOldSpaceSize: string = ''; maxOldSpaceSize: string = '';
/** How many concurrent tasks can a runner execute at a time */
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
maxConcurrency: number = 5;
} }

View File

@ -232,6 +232,7 @@ describe('GlobalConfig', () => {
launcherPath: '', launcherPath: '',
launcherRunner: 'javascript', launcherRunner: 'javascript',
maxOldSpaceSize: '', maxOldSpaceSize: '',
maxConcurrency: 5,
}, },
sentry: { sentry: {
backendDsn: '', backendDsn: '',

View File

@ -22,9 +22,11 @@
"dist/**/*" "dist/**/*"
], ],
"dependencies": { "dependencies": {
"@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*", "n8n-workflow": "workspace:*",
"n8n-core": "workspace:*", "n8n-core": "workspace:*",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"typedi": "catalog:",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,16 @@
import { Config, Env } from '@n8n/config';
@Config
export class BaseRunnerConfig {
@Env('N8N_RUNNERS_N8N_URI')
n8nUri: string = '127.0.0.1:5679';
@Env('N8N_RUNNERS_GRANT_TOKEN')
grantToken: string = '';
@Env('N8N_RUNNERS_MAX_PAYLOAD')
maxPayloadSize: number = 1024 * 1024 * 1024;
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
maxConcurrency: number = 5;
}

View File

@ -0,0 +1,10 @@
import { Config, Env } from '@n8n/config';
@Config
export class JsRunnerConfig {
@Env('NODE_FUNCTION_ALLOW_BUILTIN')
allowedBuiltInModules: string = '';
@Env('NODE_FUNCTION_ALLOW_EXTERNAL')
allowedExternalModules: string = '';
}

View File

@ -0,0 +1,13 @@
import { Config, Nested } from '@n8n/config';
import { BaseRunnerConfig } from './base-runner-config';
import { JsRunnerConfig } from './js-runner-config';
@Config
export class MainConfig {
@Nested
baseRunnerConfig!: BaseRunnerConfig;
@Nested
jsRunnerConfig!: JsRunnerConfig;
}

View File

@ -4,7 +4,6 @@ import fs from 'node:fs';
import { builtinModules } from 'node:module'; import { builtinModules } from 'node:module';
import { ValidationError } from '@/js-task-runner/errors/validation-error'; import { ValidationError } from '@/js-task-runner/errors/validation-error';
import type { JsTaskRunnerOpts } from '@/js-task-runner/js-task-runner';
import { import {
JsTaskRunner, JsTaskRunner,
type AllCodeTaskData, type AllCodeTaskData,
@ -13,17 +12,27 @@ import {
import type { Task } from '@/task-runner'; import type { Task } from '@/task-runner';
import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data'; import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data';
import type { JsRunnerConfig } from '../../config/js-runner-config';
import { MainConfig } from '../../config/main-config';
import { ExecutionError } from '../errors/execution-error'; import { ExecutionError } from '../errors/execution-error';
jest.mock('ws'); jest.mock('ws');
const defaultConfig = new MainConfig();
describe('JsTaskRunner', () => { describe('JsTaskRunner', () => {
const createRunnerWithOpts = (opts: Partial<JsTaskRunnerOpts> = {}) => const createRunnerWithOpts = (opts: Partial<JsRunnerConfig> = {}) =>
new JsTaskRunner({ new JsTaskRunner({
wsUrl: 'ws://localhost', baseRunnerConfig: {
grantToken: 'grantToken', ...defaultConfig.baseRunnerConfig,
maxConcurrency: 1, grantToken: 'grantToken',
...opts, maxConcurrency: 1,
n8nUri: 'localhost',
},
jsRunnerConfig: {
...defaultConfig.jsRunnerConfig,
...opts,
},
}); });
const defaultTaskRunner = createRunnerWithOpts(); const defaultTaskRunner = createRunnerWithOpts();

View File

@ -30,6 +30,7 @@ import { makeSerializable } from './errors/serializable-error';
import type { RequireResolver } from './require-resolver'; import type { RequireResolver } from './require-resolver';
import { createRequireResolver } from './require-resolver'; import { createRequireResolver } from './require-resolver';
import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation'; import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation';
import type { MainConfig } from '../config/main-config';
export interface JSExecSettings { export interface JSExecSettings {
code: string; code: string;
@ -76,23 +77,6 @@ export interface AllCodeTaskData {
additionalData: PartialAdditionalData; additionalData: PartialAdditionalData;
} }
export interface JsTaskRunnerOpts {
wsUrl: string;
grantToken: string;
maxConcurrency: number;
name?: string;
/**
* List of built-in nodejs modules that are allowed to be required in the
* execution sandbox. Asterisk (*) can be used to allow all.
*/
allowedBuiltInModules?: string;
/**
* List of npm modules that are allowed to be required in the execution
* sandbox. Asterisk (*) can be used to allow all.
*/
allowedExternalModules?: string;
}
type CustomConsole = { type CustomConsole = {
log: (...args: unknown[]) => void; log: (...args: unknown[]) => void;
}; };
@ -100,22 +84,20 @@ type CustomConsole = {
export class JsTaskRunner extends TaskRunner { export class JsTaskRunner extends TaskRunner {
private readonly requireResolver: RequireResolver; private readonly requireResolver: RequireResolver;
constructor({ constructor(config: MainConfig, name = 'JS Task Runner') {
grantToken, super({
maxConcurrency, taskType: 'javascript',
wsUrl, name,
name = 'JS Task Runner', ...config.baseRunnerConfig,
allowedBuiltInModules, });
allowedExternalModules, const { jsRunnerConfig } = config;
}: JsTaskRunnerOpts) {
super('javascript', wsUrl, grantToken, maxConcurrency, name);
const parseModuleAllowList = (moduleList: string) => const parseModuleAllowList = (moduleList: string) =>
moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim())); moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim()));
this.requireResolver = createRequireResolver({ this.requireResolver = createRequireResolver({
allowedBuiltInModules: parseModuleAllowList(allowedBuiltInModules ?? ''), allowedBuiltInModules: parseModuleAllowList(jsRunnerConfig.allowedBuiltInModules ?? ''),
allowedExternalModules: parseModuleAllowList(allowedExternalModules ?? ''), allowedExternalModules: parseModuleAllowList(jsRunnerConfig.allowedExternalModules ?? ''),
}); });
} }

View File

@ -1,27 +1,12 @@
import { ApplicationError, ensureError } from 'n8n-workflow'; import { ensureError } from 'n8n-workflow';
import Container from 'typedi';
import { MainConfig } from './config/main-config';
import { JsTaskRunner } from './js-task-runner/js-task-runner'; import { JsTaskRunner } from './js-task-runner/js-task-runner';
let runner: JsTaskRunner | undefined; let runner: JsTaskRunner | undefined;
let isShuttingDown = false; let isShuttingDown = false;
type Config = {
n8nUri: string;
grantToken: string;
};
function readAndParseConfig(): Config {
const grantToken = process.env.N8N_RUNNERS_GRANT_TOKEN;
if (!grantToken) {
throw new ApplicationError('Missing N8N_RUNNERS_GRANT_TOKEN environment variable');
}
return {
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? '127.0.0.1:5679',
grantToken,
};
}
function createSignalHandler(signal: string) { function createSignalHandler(signal: string) {
return async function onSignal() { return async function onSignal() {
if (isShuttingDown) { if (isShuttingDown) {
@ -46,16 +31,9 @@ function createSignalHandler(signal: string) {
} }
void (async function start() { void (async function start() {
const config = readAndParseConfig(); const config = Container.get(MainConfig);
const wsUrl = `ws://${config.n8nUri}/runners/_ws`; runner = new JsTaskRunner(config);
runner = new JsTaskRunner({
wsUrl,
grantToken: config.grantToken,
maxConcurrency: 5,
allowedBuiltInModules: process.env.NODE_FUNCTION_ALLOW_BUILTIN,
allowedExternalModules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL,
});
process.on('SIGINT', createSignalHandler('SIGINT')); process.on('SIGINT', createSignalHandler('SIGINT'));
process.on('SIGTERM', createSignalHandler('SIGTERM')); process.on('SIGTERM', createSignalHandler('SIGTERM'));

View File

@ -1,8 +1,8 @@
import { ApplicationError, type INodeTypeDescription } from 'n8n-workflow'; import { ApplicationError, type INodeTypeDescription } from 'n8n-workflow';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { URL } from 'node:url';
import { type MessageEvent, WebSocket } from 'ws'; import { type MessageEvent, WebSocket } from 'ws';
import type { BaseRunnerConfig } from './config/base-runner-config';
import { TaskRunnerNodeTypes } from './node-types'; import { TaskRunnerNodeTypes } from './node-types';
import { import {
RPC_ALLOW_LIST, RPC_ALLOW_LIST,
@ -42,7 +42,10 @@ export interface RPCCallObject {
const VALID_TIME_MS = 1000; const VALID_TIME_MS = 1000;
const VALID_EXTRA_MS = 100; const VALID_EXTRA_MS = 100;
const DEFAULT_MAX_PAYLOAD_SIZE = 1024 * 1024 * 1024; export interface TaskRunnerOpts extends BaseRunnerConfig {
taskType: string;
name?: string;
}
export abstract class TaskRunner { export abstract class TaskRunner {
id: string = nanoid(); id: string = nanoid();
@ -63,22 +66,23 @@ export abstract class TaskRunner {
nodeTypes: TaskRunnerNodeTypes = new TaskRunnerNodeTypes([]); nodeTypes: TaskRunnerNodeTypes = new TaskRunnerNodeTypes([]);
constructor( taskType: string;
public taskType: string,
wsUrl: string, maxConcurrency: number;
grantToken: string,
private maxConcurrency: number, name: string;
public name?: string,
) { constructor(opts: TaskRunnerOpts) {
const url = new URL(wsUrl); this.taskType = opts.taskType;
url.searchParams.append('id', this.id); this.name = opts.name ?? 'Node.js Task Runner SDK';
this.ws = new WebSocket(url.toString(), { this.maxConcurrency = opts.maxConcurrency;
const wsUrl = `ws://${opts.n8nUri}/runners/_ws?id=${this.id}`;
this.ws = new WebSocket(wsUrl, {
headers: { headers: {
authorization: `Bearer ${grantToken}`, authorization: `Bearer ${opts.grantToken}`,
}, },
maxPayload: process.env.N8N_RUNNERS_MAX_PAYLOAD maxPayload: opts.maxPayloadSize,
? parseInt(process.env.N8N_RUNNERS_MAX_PAYLOAD)
: DEFAULT_MAX_PAYLOAD_SIZE,
}); });
this.ws.addEventListener('message', this.receiveMessage); this.ws.addEventListener('message', this.receiveMessage);
this.ws.addEventListener('close', this.stopTaskOffers); this.ws.addEventListener('close', this.stopTaskOffers);
@ -145,7 +149,7 @@ export abstract class TaskRunner {
case 'broker:inforequest': case 'broker:inforequest':
this.send({ this.send({
type: 'runner:info', type: 'runner:info',
name: this.name ?? 'Node.js Task Runner SDK', name: this.name,
types: [this.taskType], types: [this.taskType],
}); });
break; break;

View File

@ -2,6 +2,8 @@
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": "src", "baseUrl": "src",
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]

View File

@ -179,6 +179,7 @@ export class TaskRunnerProcess extends TypedEmitter<TaskRunnerProcessEventMap> {
N8N_RUNNERS_GRANT_TOKEN: grantToken, N8N_RUNNERS_GRANT_TOKEN: grantToken,
N8N_RUNNERS_N8N_URI: n8nUri, N8N_RUNNERS_N8N_URI: n8nUri,
N8N_RUNNERS_MAX_PAYLOAD: this.runnerConfig.maxPayload.toString(), N8N_RUNNERS_MAX_PAYLOAD: this.runnerConfig.maxPayload.toString(),
N8N_RUNNERS_MAX_CONCURRENCY: this.runnerConfig.maxConcurrency.toString(),
...this.getPassthroughEnvVars(), ...this.getPassthroughEnvVars(),
}; };

View File

@ -642,6 +642,9 @@ importers:
packages/@n8n/task-runner: packages/@n8n/task-runner:
dependencies: dependencies:
'@n8n/config':
specifier: workspace:*
version: link:../config
n8n-core: n8n-core:
specifier: workspace:* specifier: workspace:*
version: link:../../core version: link:../../core
@ -651,6 +654,9 @@ importers:
nanoid: nanoid:
specifier: ^3.3.6 specifier: ^3.3.6
version: 3.3.7 version: 3.3.7
typedi:
specifier: 'catalog:'
version: 0.10.0(patch_hash=sk6omkefrosihg7lmqbzh7vfxe)
ws: ws:
specifier: '>=8.17.1' specifier: '>=8.17.1'
version: 8.17.1 version: 8.17.1