mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-03 18:27:09 +02:00
chore(core): Register ExpressionEngineConfig in @n8n/config (no-changelog) (#31383)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7975b19ced
commit
dbfd513cd0
|
|
@ -0,0 +1,123 @@
|
|||
import { Container } from '@n8n/di';
|
||||
|
||||
import { ExpressionEngineConfig } from '../expression-engine.config';
|
||||
|
||||
describe('ExpressionEngineConfig', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
Container.reset();
|
||||
jest.resetAllMocks();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('defaults', () => {
|
||||
test('bridgeTimeout defaults to 5000', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(5000);
|
||||
});
|
||||
|
||||
test('bridgeMemoryLimit defaults to 128', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeMemoryLimit).toBe(128);
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT', () => {
|
||||
test('overrides idleTimeout', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT = '60';
|
||||
const config = Container.get(ExpressionEngineConfig);
|
||||
expect(config.idleTimeout).toBe(60);
|
||||
});
|
||||
|
||||
test('parses "0" as the number 0 (distinct from undefined/unset)', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT = '0';
|
||||
const config = Container.get(ExpressionEngineConfig);
|
||||
expect(config.idleTimeout).toBe(0);
|
||||
expect(config.idleTimeout).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_EXPRESSION_ENGINE_TIMEOUT', () => {
|
||||
test('overrides bridgeTimeout', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_TIMEOUT = '1000';
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(1000);
|
||||
});
|
||||
|
||||
test('falls back to default on non-numeric value', () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
process.env.N8N_EXPRESSION_ENGINE_TIMEOUT = 'not-a-number';
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(5000);
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_EXPRESSION_ENGINE_MEMORY_LIMIT', () => {
|
||||
test('overrides bridgeMemoryLimit', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_MEMORY_LIMIT = '64';
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeMemoryLimit).toBe(64);
|
||||
});
|
||||
|
||||
test('falls back to default on non-numeric value', () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
process.env.N8N_EXPRESSION_ENGINE_MEMORY_LIMIT = 'not-a-number';
|
||||
expect(Container.get(ExpressionEngineConfig).bridgeMemoryLimit).toBe(128);
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('observability defaults', () => {
|
||||
test('observabilityEnabled defaults to true', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).observabilityEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('tracesEnabled defaults to true', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).tracesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('slowEvaluationThresholdMs defaults to 50', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).slowEvaluationThresholdMs).toBe(50);
|
||||
});
|
||||
|
||||
test('tracesSampleRate defaults to 0', () => {
|
||||
expect(Container.get(ExpressionEngineConfig).tracesSampleRate).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observability overrides', () => {
|
||||
test('N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED disables observability', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED = 'false';
|
||||
expect(Container.get(ExpressionEngineConfig).observabilityEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test('N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS overrides threshold', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS = '100';
|
||||
expect(Container.get(ExpressionEngineConfig).slowEvaluationThresholdMs).toBe(100);
|
||||
});
|
||||
|
||||
test('N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS falls back to default on non-positive value', () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
process.env.N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS = '0';
|
||||
expect(Container.get(ExpressionEngineConfig).slowEvaluationThresholdMs).toBe(50);
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE overrides sample rate', () => {
|
||||
process.env.N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE = '0.25';
|
||||
expect(Container.get(ExpressionEngineConfig).tracesSampleRate).toBe(0.25);
|
||||
});
|
||||
|
||||
test('N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE falls back to default on out-of-range value', () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
process.env.N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE = '1.5';
|
||||
expect(Container.get(ExpressionEngineConfig).tracesSampleRate).toBe(0);
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
64
packages/@n8n/config/src/configs/expression-engine.config.ts
Normal file
64
packages/@n8n/config/src/configs/expression-engine.config.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import z from 'zod';
|
||||
|
||||
import { Config, Env } from '../decorators';
|
||||
|
||||
const expressionEngineSchema = z.enum(['legacy', 'vm']);
|
||||
|
||||
@Config
|
||||
export class ExpressionEngineConfig {
|
||||
/**
|
||||
* Which expression engine to use.
|
||||
* - `legacy` runs expressions without isolation.
|
||||
* - `vm` runs expressions in a V8 isolate.
|
||||
*
|
||||
* `vm` is currently **experimental**. Use at your own risk.
|
||||
*/
|
||||
@Env('N8N_EXPRESSION_ENGINE', expressionEngineSchema)
|
||||
engine: 'legacy' | 'vm' = 'legacy';
|
||||
|
||||
/** Number of V8 isolates ready in the pool. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_POOL_SIZE')
|
||||
poolSize: number = 1;
|
||||
|
||||
/** Max number of AST-transformed expressions to cache. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE')
|
||||
maxCodeCacheSize: number = 1024;
|
||||
|
||||
/**
|
||||
* Execution timeout in milliseconds for each expression evaluation in the VM bridge.
|
||||
*/
|
||||
@Env('N8N_EXPRESSION_ENGINE_TIMEOUT')
|
||||
bridgeTimeout: number = 5000;
|
||||
|
||||
/** Memory limit in MB for the V8 isolate used by the VM bridge. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_MEMORY_LIMIT')
|
||||
bridgeMemoryLimit: number = 128;
|
||||
|
||||
/**
|
||||
* Whether to emit observability signals (metrics, traces, logs) for the VM evaluator.
|
||||
* Only takes effect when `engine === 'vm'`; legacy mode never emits expression metrics
|
||||
* regardless of this setting.
|
||||
*/
|
||||
@Env('N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED')
|
||||
observabilityEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Whether to emit OpenTelemetry spans for expression evaluation.
|
||||
* Slow evaluations (>slowEvaluationThresholdMs) and errors always emit a span.
|
||||
* Healthy-path evaluations are sampled at tracesSampleRate.
|
||||
*/
|
||||
@Env('N8N_EXPRESSION_ENGINE_TRACES_ENABLED')
|
||||
tracesEnabled: boolean = true;
|
||||
|
||||
/** Threshold in ms above which an evaluation is considered "slow" and gets a span. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS', z.number({ coerce: true }).positive())
|
||||
slowEvaluationThresholdMs: number = 50;
|
||||
|
||||
/** Head-based sampling rate (0.0–1.0) for healthy-path spans. Slow and erroring expressions always emit. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE', z.number({ coerce: true }).min(0).max(1))
|
||||
tracesSampleRate: number = 0.0;
|
||||
|
||||
/** If set, scale the pool to 0 warm isolates after this many seconds with no acquire. */
|
||||
@Env('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT')
|
||||
idleTimeout?: number;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import { DynamicBannersConfig } from './configs/dynamic-banners.config';
|
|||
import { EndpointsConfig } from './configs/endpoints.config';
|
||||
import { EventBusConfig } from './configs/event-bus.config';
|
||||
import { ExecutionsConfig } from './configs/executions.config';
|
||||
import { ExpressionEngineConfig } from './configs/expression-engine.config';
|
||||
import { ExternalHooksConfig } from './configs/external-hooks.config';
|
||||
import { GenericConfig } from './configs/generic.config';
|
||||
import { HiringBannerConfig } from './configs/hiring-banner.config';
|
||||
|
|
@ -45,6 +46,7 @@ export type { TaskRunnerMode } from './configs/runners.config';
|
|||
export { TaskRunnersConfig } from './configs/runners.config';
|
||||
export { SecurityConfig } from './configs/security.config';
|
||||
export { ExecutionsConfig } from './configs/executions.config';
|
||||
export { ExpressionEngineConfig } from './configs/expression-engine.config';
|
||||
export { LOG_SCOPES } from './configs/logging.config';
|
||||
export type { LogScope } from './configs/logging.config';
|
||||
export { WorkflowsConfig } from './configs/workflows.config';
|
||||
|
|
@ -83,6 +85,9 @@ export class GlobalConfig {
|
|||
@Nested
|
||||
publicApi: PublicApiConfig;
|
||||
|
||||
@Nested
|
||||
expressionEngine: ExpressionEngineConfig;
|
||||
|
||||
@Nested
|
||||
externalHooks: ExternalHooksConfig;
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,17 @@ describe('GlobalConfig', () => {
|
|||
maxFileSizeInKB: 10240,
|
||||
},
|
||||
},
|
||||
expressionEngine: {
|
||||
engine: 'legacy',
|
||||
poolSize: 1,
|
||||
maxCodeCacheSize: 1024,
|
||||
bridgeTimeout: 5000,
|
||||
bridgeMemoryLimit: 128,
|
||||
observabilityEnabled: true,
|
||||
tracesEnabled: true,
|
||||
slowEvaluationThresholdMs: 50,
|
||||
tracesSampleRate: 0,
|
||||
},
|
||||
externalHooks: {
|
||||
files: [],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user