perf(core): Batch items sent in runonceforeachitem mode (no-changelog) (#11870)

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
Tomi Turtiainen
2024-11-26 12:21:51 +02:00
committed by GitHub
parent 1adb730599
commit e22d0f3877
17 changed files with 457 additions and 83 deletions

View File

@@ -111,10 +111,11 @@ export class Code implements INodeType {
if (runnersConfig.enabled && language === 'javaScript') {
const code = this.getNodeParameter(codeParameterName, 0) as string;
const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this);
const numInputItems = this.getInputData().length;
return nodeMode === 'runOnceForAllItems'
? [await sandbox.runCodeAllItems()]
: [await sandbox.runCodeForEachItem()];
: [await sandbox.runCodeForEachItem(numInputItems)];
}
const getSandbox = (index = 0) => {

View File

@@ -18,6 +18,7 @@ export class JsTaskRunnerSandbox {
private readonly nodeMode: CodeExecutionMode,
private readonly workflowMode: WorkflowExecuteMode,
private readonly executeFunctions: IExecuteFunctions,
private readonly chunkSize = 1000,
) {}
async runCodeAllItems(): Promise<INodeExecutionData[]> {
@@ -39,24 +40,37 @@ export class JsTaskRunnerSandbox {
: this.throwExecutionError(executionResult.error);
}
async runCodeForEachItem(): Promise<INodeExecutionData[]> {
async runCodeForEachItem(numInputItems: number): Promise<INodeExecutionData[]> {
validateNoDisallowedMethodsInRunForEach(this.jsCode, 0);
const itemIndex = 0;
const chunks = this.chunkInputItems(numInputItems);
let executionResults: INodeExecutionData[] = [];
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
},
itemIndex,
);
for (const chunk of chunks) {
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
chunk: {
startIndex: chunk.startIdx,
count: chunk.count,
},
},
itemIndex,
);
return executionResult.ok
? executionResult.result
: this.throwExecutionError(executionResult.error);
if (!executionResult.ok) {
return this.throwExecutionError(executionResult.error);
}
executionResults = executionResults.concat(executionResult.result);
}
return executionResults;
}
private throwExecutionError(error: unknown): never {
@@ -70,4 +84,22 @@ export class JsTaskRunnerSandbox {
throw new ApplicationError(`Unknown error: ${JSON.stringify(error)}`);
}
/** Chunks the input items into chunks of 1000 items each */
private chunkInputItems(numInputItems: number) {
const numChunks = Math.ceil(numInputItems / this.chunkSize);
const chunks = [];
for (let i = 0; i < numChunks; i++) {
const startIdx = i * this.chunkSize;
const isLastChunk = i === numChunks - 1;
const count = isLastChunk ? numInputItems - startIdx : this.chunkSize;
chunks.push({
startIdx,
count,
});
}
return chunks;
}
}

View File

@@ -0,0 +1,61 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { createResultOk } from 'n8n-workflow';
import { JsTaskRunnerSandbox } from '../JsTaskRunnerSandbox';
describe('JsTaskRunnerSandbox', () => {
describe('runCodeForEachItem', () => {
it('should chunk the input items and execute the code for each chunk', async () => {
const jsCode = 'console.log($item);';
const nodeMode = 'runOnceForEachItem';
const workflowMode = 'manual';
const executeFunctions = mock<IExecuteFunctions>();
const sandbox = new JsTaskRunnerSandbox(jsCode, nodeMode, workflowMode, executeFunctions, 2);
let i = 1;
executeFunctions.startJob.mockResolvedValue(createResultOk([{ json: { item: i++ } }]));
const numInputItems = 5;
await sandbox.runCodeForEachItem(numInputItems);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(executeFunctions.startJob).toHaveBeenCalledTimes(3);
const calls = executeFunctions.startJob.mock.calls;
expect(calls).toEqual([
[
'javascript',
{
code: jsCode,
nodeMode,
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 0, count: 2 },
},
0,
],
[
'javascript',
{
code: jsCode,
nodeMode,
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 2, count: 2 },
},
0,
],
[
'javascript',
{
code: jsCode,
nodeMode,
workflowMode,
continueOnFail: executeFunctions.continueOnFail(),
chunk: { startIndex: 4, count: 1 },
},
0,
],
]);
});
});
});