feat: Enable partial exections v2 by default (#13344)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Danny Martini 2025-02-21 10:25:53 +01:00 committed by GitHub
parent 073b05b10c
commit 29ae2396c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 115 additions and 92 deletions

View file

@ -489,7 +489,11 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => { cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object'); expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users']; const expectedKeys = [
'When clicking Test workflow',
'fetch 5 random users',
'do something with them',
];
const { runData } = interception.request.body as Record<string, object>; const { runData } = interception.request.body as Record<string, object>;
expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length); expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);

View file

@ -4,7 +4,7 @@ const canvas = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Manual partial execution', () => { describe('Manual partial execution', () => {
it('should execute parent nodes with no run data only once', () => { it('should not execute parent nodes with no run data', () => {
canvas.actions.visit(); canvas.actions.visit();
cy.fixture('manual-partial-execution.json').then((data) => { cy.fixture('manual-partial-execution.json').then((data) => {
@ -22,8 +22,8 @@ describe('Manual partial execution', () => {
canvas.actions.openNode('Webhook1'); canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist'); ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunTooltipIndicator().should('exist'); ndv.getters.nodeRunTooltipIndicator().should('not.exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run ndv.getters.outputRunSelector().should('not.exist');
}); });
}); });

View file

@ -181,6 +181,5 @@ export interface FrontendSettings {
easyAIWorkflowOnboarded: boolean; easyAIWorkflowOnboarded: boolean;
partialExecution: { partialExecution: {
version: 1 | 2; version: 1 | 2;
enforce: boolean;
}; };
} }

View file

@ -4,9 +4,5 @@ import { Config, Env } from '../decorators';
export class PartialExecutionsConfig { export class PartialExecutionsConfig {
/** Partial execution logic version to use by default. */ /** Partial execution logic version to use by default. */
@Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT') @Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT')
version: 1 | 2 = 1; version: 1 | 2 = 2;
/** Set this to true to enforce using the default version. Users cannot use the other version then by setting a local storage key. */
@Env('N8N_PARTIAL_EXECUTION_ENFORCE_VERSION')
enforce: boolean = false;
} }

View file

@ -305,8 +305,7 @@ describe('GlobalConfig', () => {
disabled: false, disabled: false,
}, },
partialExecutions: { partialExecutions: {
version: 1, version: 2,
enforce: false,
}, },
}; };

View file

@ -1,15 +1,69 @@
import { mockLogger } from '@test/mocking'; import { captor, mock } from 'jest-mock-extended';
import type { Logger } from 'n8n-core';
import { DeprecationService } from '../deprecation.service'; import { DeprecationService } from '../deprecation.service';
describe('DeprecationService', () => { describe('DeprecationService', () => {
const toTest = (envVar: string, value: string, mustWarn: boolean) => { const logger = mock<Logger>();
process.env[envVar] = value; const deprecationService = new DeprecationService(logger);
const deprecationService = new DeprecationService(mockLogger());
beforeEach(() => {
// Ignore environment variables coming in from the environment when running
// this test suite.
process.env = {};
jest.resetAllMocks();
});
describe('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT', () => {
test('supports multiple warnings for the same environment variable', () => {
// ARRANGE
process.env.N8N_PARTIAL_EXECUTION_VERSION_DEFAULT = '1';
const dataCaptor = captor();
// ACT
deprecationService.warn(); deprecationService.warn();
expect(deprecationService.mustWarn(envVar)).toBe(mustWarn); // ASSERT
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith(dataCaptor);
expect(dataCaptor.value.split('\n')).toEqual(
expect.arrayContaining([
' - N8N_PARTIAL_EXECUTION_VERSION_DEFAULT -> Version 1 of partial executions is deprecated and will be removed as early as v1.85.0',
' - N8N_PARTIAL_EXECUTION_VERSION_DEFAULT -> This environment variable is internal and should not be set.',
]),
);
});
});
const toTest = (envVar: string, value: string | undefined, mustWarn: boolean) => {
const originalEnv = process.env[envVar];
try {
// ARRANGE
if (value) {
process.env[envVar] = value;
} else {
delete process.env[envVar];
}
// ACT
deprecationService.warn();
// ASSERT
if (mustWarn) {
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn.mock.lastCall?.[0]).toMatch(envVar);
} else {
expect(logger.warn.mock.lastCall?.[0] ?? '').not.toMatch(envVar);
}
} finally {
// CLEANUP
if (originalEnv) {
process.env[envVar] = originalEnv;
} else {
delete process.env[envVar];
}
}
}; };
test.each([ test.each([
@ -18,7 +72,10 @@ describe('DeprecationService', () => {
['EXECUTIONS_DATA_PRUNE_TIMEOUT', '1', true], ['EXECUTIONS_DATA_PRUNE_TIMEOUT', '1', true],
['N8N_CONFIG_FILES', '1', true], ['N8N_CONFIG_FILES', '1', true],
['N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN', '1', true], ['N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN', '1', true],
])('should detect when %s is in use', (envVar, value, mustWarn) => { ['N8N_PARTIAL_EXECUTION_VERSION_DEFAULT', '1', true],
['N8N_PARTIAL_EXECUTION_VERSION_DEFAULT', '2', true],
['N8N_PARTIAL_EXECUTION_VERSION_DEFAULT', undefined, false],
])('should detect when %s is `%s`', (envVar, value, mustWarn) => {
toTest(envVar, value, mustWarn); toTest(envVar, value, mustWarn);
}); });
@ -48,15 +105,7 @@ describe('DeprecationService', () => {
['true', false], ['true', false],
[undefined /* warnIfMissing */, true], [undefined /* warnIfMissing */, true],
])('should handle value: %s', (value, mustWarn) => { ])('should handle value: %s', (value, mustWarn) => {
if (value === undefined) { toTest(envVar, value, mustWarn);
delete process.env[envVar];
} else {
process.env[envVar] = value;
}
const deprecationService = new DeprecationService(mockLogger());
deprecationService.warn();
expect(deprecationService.mustWarn(envVar)).toBe(mustWarn);
}); });
}); });
}); });

View file

@ -1,6 +1,5 @@
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
type EnvVarName = string; type EnvVarName = string;
@ -49,32 +48,41 @@ export class DeprecationService {
checkValue: (value?: string) => value?.toLowerCase() !== 'true' && value !== '1', checkValue: (value?: string) => value?.toLowerCase() !== 'true' && value !== '1',
warnIfMissing: true, warnIfMissing: true,
}, },
{
envVar: 'N8N_PARTIAL_EXECUTION_VERSION_DEFAULT',
checkValue: (value: string) => value === '1',
message:
'Version 1 of partial executions is deprecated and will be removed as early as v1.85.0',
},
{
envVar: 'N8N_PARTIAL_EXECUTION_VERSION_DEFAULT',
message: 'This environment variable is internal and should not be set.',
},
]; ];
/** Runtime state of deprecation-related env vars. */ /** Runtime state of deprecation-related env vars. */
private readonly state: Record<EnvVarName, { mustWarn: boolean }> = {}; private readonly state: Map<Deprecation, { mustWarn: boolean }> = new Map();
constructor(private readonly logger: Logger) {} constructor(private readonly logger: Logger) {}
warn() { warn() {
this.deprecations.forEach((d) => { this.deprecations.forEach((d) => {
const envValue = process.env[d.envVar]; const envValue = process.env[d.envVar];
this.state[d.envVar] = { this.state.set(d, {
mustWarn: mustWarn:
(d.warnIfMissing !== undefined && envValue === undefined) || (d.warnIfMissing !== undefined && envValue === undefined) ||
(d.checkValue ? d.checkValue(envValue) : envValue !== undefined), (d.checkValue ? d.checkValue(envValue) : envValue !== undefined),
}; });
}); });
const mustWarn = Object.entries(this.state) const mustWarn: Deprecation[] = [];
.filter(([, d]) => d.mustWarn) for (const [deprecation, metadata] of this.state.entries()) {
.map(([envVar]) => { if (!metadata.mustWarn) {
const deprecation = this.deprecations.find((d) => d.envVar === envVar); continue;
if (!deprecation) { }
throw new ApplicationError(`Deprecation not found for env var: ${envVar}`);
mustWarn.push(deprecation);
} }
return deprecation;
});
if (mustWarn.length === 0) return; if (mustWarn.length === 0) return;
@ -87,8 +95,4 @@ export class DeprecationService {
this.logger.warn(`\n${header}:\n${deprecations}`); this.logger.warn(`\n${header}:\n${deprecations}`);
} }
mustWarn(envVar: string) {
return this.state[envVar]?.mustWarn ?? false;
}
} }

View file

@ -138,7 +138,6 @@ export const defaultSettings: FrontendSettings = {
easyAIWorkflowOnboarded: false, easyAIWorkflowOnboarded: false,
partialExecution: { partialExecution: {
version: 1, version: 1,
enforce: false,
}, },
folders: { folders: {
enabled: false, enabled: false,

View file

@ -117,44 +117,32 @@ describe('settings.store', () => {
{ {
name: 'pick the default', name: 'pick the default',
default: 1 as const, default: 1 as const,
enforce: false,
userVersion: -1, userVersion: -1,
result: 1, result: 1,
}, },
{ {
name: "pick the user' choice", name: 'pick the default',
default: 1 as const, default: 2 as const,
enforce: false, userVersion: -1,
userVersion: 2,
result: 2, result: 2,
}, },
{ {
name: 'enforce the default', name: "pick the user's choice",
default: 1 as const, default: 1 as const,
enforce: true,
userVersion: 2, userVersion: 2,
result: 1,
},
{
name: 'enforce the default',
default: 2 as const,
enforce: true,
userVersion: 1,
result: 2, result: 2,
}, },
{ {
name: 'handle values that used to be allowed in local storage', name: 'handle values that used to be allowed in local storage',
default: 1 as const, default: 1 as const,
enforce: false,
userVersion: 0, userVersion: 0,
result: 1, result: 1,
}, },
])('%name', async ({ default: defaultVersion, userVersion, enforce, result }) => { ])('%name', async ({ default: defaultVersion, userVersion, result }) => {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
settingsStore.settings.partialExecution = { settingsStore.settings.partialExecution = {
version: defaultVersion, version: defaultVersion,
enforce,
}; };
vi.mocked(useLocalStorage).mockReturnValueOnce(ref(userVersion)); vi.mocked(useLocalStorage).mockReturnValueOnce(ref(userVersion));

View file

@ -103,16 +103,11 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const partialExecutionVersion = computed(() => { const partialExecutionVersion = computed(() => {
const defaultVersion = settings.value.partialExecution?.version ?? 1; const defaultVersion = settings.value.partialExecution?.version ?? 1;
const enforceVersion = settings.value.partialExecution?.enforce ?? false;
// -1 means we pick the defaultVersion // -1 means we pick the defaultVersion
// 1 is the old flow // 1 is the old flow
// 2 is the new flow // 2 is the new flow
const userVersion = useLocalStorage('PartialExecution.version', -1).value; const userVersion = useLocalStorage('PartialExecution.version', -1).value;
const version = enforceVersion const version = userVersion === -1 ? defaultVersion : userVersion;
? defaultVersion
: userVersion === -1
? defaultVersion
: userVersion;
// For backwards compatibility, e.g. if the user has 0 in their local // For backwards compatibility, e.g. if the user has 0 in their local
// storage, which used to be allowed, but not anymore. // storage, which used to be allowed, but not anymore.

View file

@ -682,31 +682,21 @@ describe('useWorkflowsStore', () => {
}); });
test.each([ test.each([
// enforce true cases - the version is always the defaultVersion // check userVersion behavior
[-1, 1, true, 1], // enforce true, use default (1) [-1, 1, 1], // userVersion -1, use default (1)
[0, 1, true, 1], // enforce true, use default (1) [0, 1, 1], // userVersion 0, invalid, use default (1)
[1, 1, true, 1], // enforce true, use default (1) [1, 1, 1], // userVersion 1, valid, use userVersion (1)
[2, 1, true, 1], // enforce true, use default (1) [2, 1, 2], // userVersion 2, valid, use userVersion (2)
[-1, 2, true, 2], // enforce true, use default (2) [-1, 2, 2], // userVersion -1, use default (2)
[0, 2, true, 2], // enforce true, use default (2) [0, 2, 1], // userVersion 0, invalid, use default (2)
[1, 2, true, 2], // enforce true, use default (2) [1, 2, 1], // userVersion 1, valid, use userVersion (1)
[2, 2, true, 2], // enforce true, use default (2) [2, 2, 2], // userVersion 2, valid, use userVersion (2)
] as Array<[number, 1 | 2, number]>)(
// enforce false cases - check userVersion behavior
[-1, 1, false, 1], // userVersion -1, use default (1)
[0, 1, false, 1], // userVersion 0, invalid, use default (1)
[1, 1, false, 1], // userVersion 1, valid, use userVersion (1)
[2, 1, false, 2], // userVersion 2, valid, use userVersion (2)
[-1, 2, false, 2], // userVersion -1, use default (2)
[0, 2, false, 1], // userVersion 0, invalid, use default (2)
[1, 2, false, 1], // userVersion 1, valid, use userVersion (1)
[2, 2, false, 2], // userVersion 2, valid, use userVersion (2)
] as Array<[number, 1 | 2, boolean, number]>)(
'when { userVersion:%s, defaultVersion:%s, enforced:%s } run workflow should use partial execution version %s', 'when { userVersion:%s, defaultVersion:%s, enforced:%s } run workflow should use partial execution version %s',
async (userVersion, defaultVersion, enforce, expectedVersion) => { async (userVersion, defaultVersion, expectedVersion) => {
vi.mocked(useLocalStorage).mockReturnValueOnce(ref(userVersion)); vi.mocked(useLocalStorage).mockReturnValueOnce(ref(userVersion));
settingsStore.settings = { settingsStore.settings = {
partialExecution: { version: defaultVersion, enforce }, partialExecution: { version: defaultVersion },
} as FrontendSettings; } as FrontendSettings;
const workflowData = { id: '1', nodes: [], connections: {} }; const workflowData = { id: '1', nodes: [], connections: {} };