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) => {
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>;
expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length);

View file

@ -4,7 +4,7 @@ const canvas = new WorkflowPage();
const ndv = new NDV();
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();
cy.fixture('manual-partial-execution.json').then((data) => {
@ -22,8 +22,8 @@ describe('Manual partial execution', () => {
canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
ndv.getters.outputRunSelector().should('not.exist');
});
});

View file

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

View file

@ -4,9 +4,5 @@ import { Config, Env } from '../decorators';
export class PartialExecutionsConfig {
/** Partial execution logic version to use by default. */
@Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT')
version: 1 | 2 = 1;
/** 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;
version: 1 | 2 = 2;
}

View file

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

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';
describe('DeprecationService', () => {
const toTest = (envVar: string, value: string, mustWarn: boolean) => {
process.env[envVar] = value;
const deprecationService = new DeprecationService(mockLogger());
const logger = mock<Logger>();
const deprecationService = new DeprecationService(logger);
deprecationService.warn();
beforeEach(() => {
// Ignore environment variables coming in from the environment when running
// this test suite.
process.env = {};
expect(deprecationService.mustWarn(envVar)).toBe(mustWarn);
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();
// 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([
@ -18,7 +72,10 @@ describe('DeprecationService', () => {
['EXECUTIONS_DATA_PRUNE_TIMEOUT', '1', true],
['N8N_CONFIG_FILES', '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);
});
@ -48,15 +105,7 @@ describe('DeprecationService', () => {
['true', false],
[undefined /* warnIfMissing */, true],
])('should handle value: %s', (value, mustWarn) => {
if (value === undefined) {
delete process.env[envVar];
} else {
process.env[envVar] = value;
}
const deprecationService = new DeprecationService(mockLogger());
deprecationService.warn();
expect(deprecationService.mustWarn(envVar)).toBe(mustWarn);
toTest(envVar, value, mustWarn);
});
});
});

View file

@ -1,6 +1,5 @@
import { Service } from '@n8n/di';
import { Logger } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
type EnvVarName = string;
@ -49,32 +48,41 @@ export class DeprecationService {
checkValue: (value?: string) => value?.toLowerCase() !== 'true' && value !== '1',
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. */
private readonly state: Record<EnvVarName, { mustWarn: boolean }> = {};
private readonly state: Map<Deprecation, { mustWarn: boolean }> = new Map();
constructor(private readonly logger: Logger) {}
warn() {
this.deprecations.forEach((d) => {
const envValue = process.env[d.envVar];
this.state[d.envVar] = {
this.state.set(d, {
mustWarn:
(d.warnIfMissing !== undefined && envValue === undefined) ||
(d.checkValue ? d.checkValue(envValue) : envValue !== undefined),
};
});
});
const mustWarn = Object.entries(this.state)
.filter(([, d]) => d.mustWarn)
.map(([envVar]) => {
const deprecation = this.deprecations.find((d) => d.envVar === envVar);
if (!deprecation) {
throw new ApplicationError(`Deprecation not found for env var: ${envVar}`);
}
return deprecation;
});
const mustWarn: Deprecation[] = [];
for (const [deprecation, metadata] of this.state.entries()) {
if (!metadata.mustWarn) {
continue;
}
mustWarn.push(deprecation);
}
if (mustWarn.length === 0) return;
@ -87,8 +95,4 @@ export class DeprecationService {
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,
partialExecution: {
version: 1,
enforce: false,
},
folders: {
enabled: false,

View file

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

View file

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

View file

@ -682,31 +682,21 @@ describe('useWorkflowsStore', () => {
});
test.each([
// enforce true cases - the version is always the defaultVersion
[-1, 1, true, 1], // enforce true, use default (1)
[0, 1, true, 1], // enforce true, use default (1)
[1, 1, true, 1], // enforce true, use default (1)
[2, 1, true, 1], // enforce true, use default (1)
[-1, 2, true, 2], // enforce true, use default (2)
[0, 2, true, 2], // enforce true, use default (2)
[1, 2, true, 2], // enforce true, use default (2)
[2, 2, true, 2], // enforce true, use default (2)
// 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]>)(
// check userVersion behavior
[-1, 1, 1], // userVersion -1, use default (1)
[0, 1, 1], // userVersion 0, invalid, use default (1)
[1, 1, 1], // userVersion 1, valid, use userVersion (1)
[2, 1, 2], // userVersion 2, valid, use userVersion (2)
[-1, 2, 2], // userVersion -1, use default (2)
[0, 2, 1], // userVersion 0, invalid, use default (2)
[1, 2, 1], // userVersion 1, valid, use userVersion (1)
[2, 2, 2], // userVersion 2, valid, use userVersion (2)
] as Array<[number, 1 | 2, number]>)(
'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));
settingsStore.settings = {
partialExecution: { version: defaultVersion, enforce },
partialExecution: { version: defaultVersion },
} as FrontendSettings;
const workflowData = { id: '1', nodes: [], connections: {} };