feat(core): Make the vm expression engine the default on 1.x (#36498)
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.13.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mike Repeć 2026-08-18 14:54:47 +02:00 committed by GitHub
parent 4ee48e61a1
commit 75bedd8eaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 42 additions and 12 deletions

View File

@ -16,6 +16,10 @@ describe('ExpressionEngineConfig', () => {
});
describe('defaults', () => {
test('engine defaults to vm', () => {
expect(Container.get(ExpressionEngineConfig).engine).toBe('vm');
});
test('bridgeTimeout defaults to 5000', () => {
expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(5000);
});
@ -25,6 +29,21 @@ describe('ExpressionEngineConfig', () => {
});
});
describe('N8N_EXPRESSION_ENGINE', () => {
test('overrides engine to legacy', () => {
process.env.N8N_EXPRESSION_ENGINE = 'legacy';
expect(Container.get(ExpressionEngineConfig).engine).toBe('legacy');
});
test('falls back to default on invalid value', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.N8N_EXPRESSION_ENGINE = 'not-an-engine';
expect(Container.get(ExpressionEngineConfig).engine).toBe('vm');
expect(consoleWarnSpy).toHaveBeenCalled();
});
});
describe('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT', () => {
test('overrides idleTimeout', () => {
process.env.N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT = '60';

View File

@ -8,13 +8,11 @@ const expressionEngineSchema = z.enum(['legacy', 'vm']);
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.
* - `vm` (default) runs expressions in a V8 isolate.
* - `legacy` runs expressions without isolation. Less secure and soon to be deprecated.
*/
@Env('N8N_EXPRESSION_ENGINE', expressionEngineSchema)
engine: 'legacy' | 'vm' = 'legacy';
engine: 'legacy' | 'vm' = 'vm';
/** Number of V8 isolates ready in the pool. */
@Env('N8N_EXPRESSION_ENGINE_POOL_SIZE')

View File

@ -153,7 +153,7 @@ describe('GlobalConfig', () => {
},
},
expressionEngine: {
engine: 'legacy',
engine: 'vm',
poolSize: 1,
maxCodeCacheSize: 1024,
bridgeTimeout: 5000,

View File

@ -15,7 +15,7 @@ Implemented so far:
- ✅ Integration tests (PR 4)
Coming in later PRs:
- 🚧 Workflow integration behind `N8N_EXPRESSION_ENGINE=vm` flag (PR 5)
- ✅ Workflow integration via `N8N_EXPRESSION_ENGINE` (default: `vm`; set `legacy` to opt out) (PR 5)
- 🚧 Web Worker support (Phase 2+)
- 🚧 Performance optimizations (Phase 3)

View File

@ -11,6 +11,7 @@ import { ActiveExecutions } from '@/active-executions';
import { DeprecationService } from '@/deprecation/deprecation.service';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider';
import { ExternalHooks } from '@/external-hooks';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service';
@ -31,6 +32,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
const shutdownService = mockInstance(ShutdownService);
const deprecationService = mockInstance(DeprecationService);
mockInstance(MessageEventBus);
mockInstance(ExpressionObservabilityProvider);
const posthogClient = mockInstance(PostHogClient);
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
const externalHooks = mockInstance(ExternalHooks);

View File

@ -12,6 +12,7 @@ import { ActiveExecutions } from '@/active-executions';
import { DeprecationService } from '@/deprecation/deprecation.service';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider';
import { ExternalHooks } from '@/external-hooks';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service';
@ -30,6 +31,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
const shutdownService = mockInstance(ShutdownService);
const deprecationService = mockInstance(DeprecationService);
mockInstance(MessageEventBus);
mockInstance(ExpressionObservabilityProvider);
const posthogClient = mockInstance(PostHogClient);
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
const externalHooks = mockInstance(ExternalHooks);

View File

@ -63,6 +63,8 @@ describe('DeprecationService', () => {
['EXECUTIONS_DATA_PRUNE_TIMEOUT', '1', true],
['N8N_CONFIG_FILES', '1', true],
['N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN', '1', true],
['N8N_EXPRESSION_ENGINE', 'legacy', true],
['N8N_EXPRESSION_ENGINE', 'vm', false],
])('should detect when %s is `%s`', (envVar, value, mustWarn) => {
toTest(envVar, value, mustWarn);
});

View File

@ -91,6 +91,12 @@ export class DeprecationService {
'n8n does not support `own` mode since May 2023. Please remove this environment variable to allow n8n to start. If you need the isolation and performance gains, please consider queue mode: https://docs.n8n.io/hosting/scaling/queue-mode/',
checkValue: (value: string) => value === 'own',
},
{
envVar: 'N8N_EXPRESSION_ENGINE',
message:
'The `legacy` expression engine runs expressions without isolation, is no longer considered secure, and will be removed in a future version. Remove this environment variable to use the default `vm` engine.',
checkValue: (value?: string) => value === 'legacy',
},
{
envVar: 'N8N_BLOCK_ENV_ACCESS_IN_NODE',
message:

View File

@ -252,7 +252,7 @@ export class Expression {
}
/**
* Initialize the VM evaluator (if feature flag is enabled).
* Initialize the VM evaluator (no-op when the legacy engine is selected).
* Should be called once during application startup.
* Only available in Node.js environments (not in browser).
*/
@ -660,7 +660,7 @@ export class Expression {
if (Expression.expressionEngine === 'vm' && !IS_FRONTEND) {
if (!Expression.vmEvaluator) {
throw new ApplicationError(
'N8N_EXPRESSION_ENGINE=vm is enabled but VM evaluator is not initialized. Call Expression.initExpressionEngine() during application startup.',
'The VM expression engine has not been initialized. Call Expression.initExpressionEngine() during application startup.',
);
}

View File

@ -15,14 +15,15 @@ export default defineConfig({
{
test: {
...sharedTestConfig,
name: 'legacy-engine',
name: 'vm-engine',
env: { N8N_EXPRESSION_ENGINE: 'vm' },
},
},
{
test: {
...sharedTestConfig,
name: 'vm-engine',
env: { N8N_EXPRESSION_ENGINE: 'vm' },
name: 'legacy-engine',
env: { N8N_EXPRESSION_ENGINE: 'legacy' },
},
},
],