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:
parent
ea47b025fb
commit
d7ba206b30
@ -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"
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -232,6 +232,7 @@ describe('GlobalConfig', () => {
|
|||||||
launcherPath: '',
|
launcherPath: '',
|
||||||
launcherRunner: 'javascript',
|
launcherRunner: 'javascript',
|
||||||
maxOldSpaceSize: '',
|
maxOldSpaceSize: '',
|
||||||
|
maxConcurrency: 5,
|
||||||
},
|
},
|
||||||
sentry: {
|
sentry: {
|
||||||
backendDsn: '',
|
backendDsn: '',
|
||||||
|
@ -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": {
|
||||||
|
16
packages/@n8n/task-runner/src/config/base-runner-config.ts
Normal file
16
packages/@n8n/task-runner/src/config/base-runner-config.ts
Normal 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;
|
||||||
|
}
|
10
packages/@n8n/task-runner/src/config/js-runner-config.ts
Normal file
10
packages/@n8n/task-runner/src/config/js-runner-config.ts
Normal 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 = '';
|
||||||
|
}
|
13
packages/@n8n/task-runner/src/config/main-config.ts
Normal file
13
packages/@n8n/task-runner/src/config/main-config.ts
Normal 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;
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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 ?? ''),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'));
|
||||||
|
@ -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;
|
||||||
|
@ -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": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user