mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
⚡ Implement timeout for workers and corrected timeout for subworkflows (#1634)
* Implemented timeout for workers and corrected timeout for subworkflows
* Fixed issue with timeouts when running on separate processes
* Standardized timeouts across all n8n
Now the maxTimeout setting takes effect whenever a timeout is set
to any workflows.
This causes local timeouts (either set on a per-workflow basis or
via global settings) to be capped by the maximum timeout. This
behavior already existed but was not applied to all places.
Also changed the way n8n enforces timeouts for subworkflows, making
it work always.
In effect, with this change, if you have one workflow that calls others
only the main workflow's timeout is taken into consideration, limiting
the maximum time that other workflows combined can run.
* ⚡ Fix timeout problem in "own" mode
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
882e2f8e74
commit
0c779de704
|
@ -127,11 +127,22 @@ export class Worker extends Command {
|
||||||
staticData = workflowData.staticData;
|
staticData = workflowData.staticData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
|
if (currentExecutionDb.workflowData.settings && currentExecutionDb.workflowData.settings.executionTimeout) {
|
||||||
|
workflowTimeout = currentExecutionDb.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
||||||
|
}
|
||||||
|
|
||||||
|
let executionTimeoutTimestamp: number | undefined;
|
||||||
|
if (workflowTimeout > 0) {
|
||||||
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
||||||
|
executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings });
|
const workflow = new Workflow({ id: currentExecutionDb.workflowData.id as string, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData!.nodes, connections: currentExecutionDb.workflowData!.connections, active: currentExecutionDb.workflowData!.active, nodeTypes, staticData, settings: currentExecutionDb.workflowData!.settings });
|
||||||
|
|
||||||
const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes);
|
const credentials = await WorkflowCredentials(currentExecutionDb.workflowData.nodes);
|
||||||
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials, undefined, executionTimeoutTimestamp);
|
||||||
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
|
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(currentExecutionDb.mode, job.data.executionId, currentExecutionDb.workflowData, { retryOf: currentExecutionDb.retryOf as string });
|
||||||
|
|
||||||
let workflowExecute: WorkflowExecute;
|
let workflowExecute: WorkflowExecute;
|
||||||
|
|
|
@ -11,11 +11,12 @@ import {
|
||||||
ITaskData,
|
ITaskData,
|
||||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDeferredPromise,
|
IDeferredPromise, WorkflowExecute,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
|
|
||||||
import * as PCancelable from 'p-cancelable';
|
import * as PCancelable from 'p-cancelable';
|
||||||
|
@ -410,3 +411,9 @@ export interface IWorkflowExecutionDataProcessWithExecution extends IWorkflowExe
|
||||||
executionId: string;
|
executionId: string;
|
||||||
nodeTypeData: ITransferNodeTypes;
|
nodeTypeData: ITransferNodeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkflowExecuteProcess {
|
||||||
|
startedAt: Date,
|
||||||
|
workflow: Workflow;
|
||||||
|
workflowExecute: WorkflowExecute;
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IPushDataExecutionFinished,
|
IPushDataExecutionFinished,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
|
IWorkflowExecuteProcess,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
Push,
|
Push,
|
||||||
|
@ -569,7 +570,7 @@ export async function getWorkflowData(workflowInfo: IExecuteWorkflowInfo): Promi
|
||||||
* @param {INodeExecutionData[]} [inputData]
|
* @param {INodeExecutionData[]} [inputData]
|
||||||
* @returns {(Promise<Array<INodeExecutionData[] | null>>)}
|
* @returns {(Promise<Array<INodeExecutionData[] | null>>)}
|
||||||
*/
|
*/
|
||||||
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IRun> {
|
export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[], parentExecutionId?: string, loadedWorkflowData?: IWorkflowBase, loadedRunData?: IWorkflowExecutionDataProcess): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
|
@ -605,10 +606,19 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
// This one already contains changes to talk to parent process
|
// This one already contains changes to talk to parent process
|
||||||
// and get executionID from `activeExecutions` running on main process
|
// and get executionID from `activeExecutions` running on main process
|
||||||
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
additionalDataIntegrated.executeWorkflow = additionalData.executeWorkflow;
|
||||||
|
additionalDataIntegrated.executionTimeoutTimestamp = additionalData.executionTimeoutTimestamp;
|
||||||
|
|
||||||
|
|
||||||
// Execute the workflow
|
// Execute the workflow
|
||||||
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
|
const workflowExecute = new WorkflowExecute(additionalDataIntegrated, runData.executionMode, runExecutionData);
|
||||||
|
if (parentExecutionId !== undefined) {
|
||||||
|
// Must be changed to become typed
|
||||||
|
return {
|
||||||
|
startedAt: new Date(),
|
||||||
|
workflow,
|
||||||
|
workflowExecute,
|
||||||
|
};
|
||||||
|
}
|
||||||
const data = await workflowExecute.processRunExecutionData(workflow);
|
const data = await workflowExecute.processRunExecutionData(workflow);
|
||||||
|
|
||||||
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
||||||
|
@ -616,14 +626,9 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
if (data.finished === true) {
|
if (data.finished === true) {
|
||||||
// Workflow did finish successfully
|
// Workflow did finish successfully
|
||||||
|
|
||||||
if (parentExecutionId !== undefined) {
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
await ActiveExecutions.getInstance().remove(executionId, data);
|
await ActiveExecutions.getInstance().remove(executionId, data);
|
||||||
|
|
||||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||||
return returnData!.data!.main;
|
return returnData!.data!.main;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await ActiveExecutions.getInstance().remove(executionId, data);
|
await ActiveExecutions.getInstance().remove(executionId, data);
|
||||||
// Workflow did fail
|
// Workflow did fail
|
||||||
|
@ -644,7 +649,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
|
||||||
* @param {INodeParameters} currentNodeParameters
|
* @param {INodeParameters} currentNodeParameters
|
||||||
* @returns {Promise<IWorkflowExecuteAdditionalData>}
|
* @returns {Promise<IWorkflowExecuteAdditionalData>}
|
||||||
*/
|
*/
|
||||||
export async function getBase(credentials: IWorkflowCredentials, currentNodeParameters?: INodeParameters): Promise<IWorkflowExecuteAdditionalData> {
|
export async function getBase(credentials: IWorkflowCredentials, currentNodeParameters?: INodeParameters, executionTimeoutTimestamp?: number): Promise<IWorkflowExecuteAdditionalData> {
|
||||||
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
||||||
|
|
||||||
const timezone = config.get('generic.timezone') as string;
|
const timezone = config.get('generic.timezone') as string;
|
||||||
|
@ -666,6 +671,7 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
|
||||||
webhookBaseUrl,
|
webhookBaseUrl,
|
||||||
webhookTestBaseUrl,
|
webhookTestBaseUrl,
|
||||||
currentNodeParameters,
|
currentNodeParameters,
|
||||||
|
executionTimeoutTimestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,8 +158,22 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
||||||
|
|
||||||
|
// Soft timeout to stop workflow execution after current running node
|
||||||
|
// Changes were made by adding the `workflowTimeout` to the `additionalData`
|
||||||
|
// So that the timeout will also work for executions with nested workflows.
|
||||||
|
let executionTimeout: NodeJS.Timeout;
|
||||||
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
|
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
||||||
|
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowTimeout > 0) {
|
||||||
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
||||||
|
}
|
||||||
|
|
||||||
const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData });
|
const workflow = new Workflow({ id: data.workflowData.id as string | undefined, name: data.workflowData.name, nodes: data.workflowData!.nodes, connections: data.workflowData!.connections, active: data.workflowData!.active, nodeTypes, staticData: data.workflowData!.staticData });
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
|
||||||
|
|
||||||
// Register the active execution
|
// Register the active execution
|
||||||
const executionId = await this.activeExecutions.add(data, undefined);
|
const executionId = await this.activeExecutions.add(data, undefined);
|
||||||
|
@ -184,12 +198,6 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
||||||
|
|
||||||
// Soft timeout to stop workflow execution after current running node
|
|
||||||
let executionTimeout: NodeJS.Timeout;
|
|
||||||
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default
|
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
|
||||||
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workflowTimeout) {
|
if (workflowTimeout) {
|
||||||
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
||||||
|
@ -280,7 +288,6 @@ export class WorkflowRunner {
|
||||||
* the database. *
|
* the database. *
|
||||||
*************************************************/
|
*************************************************/
|
||||||
let watchDogInterval: NodeJS.Timeout | undefined;
|
let watchDogInterval: NodeJS.Timeout | undefined;
|
||||||
let resolved = false;
|
|
||||||
|
|
||||||
const watchDog = new Promise((res) => {
|
const watchDog = new Promise((res) => {
|
||||||
watchDogInterval = setInterval(async () => {
|
watchDogInterval = setInterval(async () => {
|
||||||
|
@ -301,28 +308,9 @@ export class WorkflowRunner {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await new Promise((res, rej) => {
|
await Promise.race([jobData, watchDog]);
|
||||||
jobData.then((data) => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
clearWatchdogInterval();
|
clearWatchdogInterval();
|
||||||
res(data);
|
|
||||||
}
|
|
||||||
}).catch((e) => {
|
|
||||||
if(!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
clearWatchdogInterval();
|
|
||||||
rej(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
watchDog.then((data) => {
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
clearWatchdogInterval();
|
|
||||||
res(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await jobData;
|
await jobData;
|
||||||
}
|
}
|
||||||
|
@ -383,7 +371,7 @@ export class WorkflowRunner {
|
||||||
* @memberof WorkflowRunner
|
* @memberof WorkflowRunner
|
||||||
*/
|
*/
|
||||||
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
|
async runSubprocess(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
|
||||||
const startedAt = new Date();
|
let startedAt = new Date();
|
||||||
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
||||||
|
|
||||||
if (loadStaticData === true && data.workflowData.id) {
|
if (loadStaticData === true && data.workflowData.id) {
|
||||||
|
@ -426,7 +414,6 @@ export class WorkflowRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).executionId = executionId;
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).nodeTypeData = nodeTypeData;
|
||||||
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
|
(data as unknown as IWorkflowExecutionDataProcessWithExecution).credentialsOverwrite = credentialsOverwrites;
|
||||||
|
@ -439,24 +426,40 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
// Start timeout for the execution
|
// Start timeout for the execution
|
||||||
let executionTimeout: NodeJS.Timeout;
|
let executionTimeout: NodeJS.Timeout;
|
||||||
let workflowTimeout = config.get('executions.timeout') as number > 0 && config.get('executions.timeout') as number; // initialize with default
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
if (data.workflowData.settings && data.workflowData.settings.executionTimeout) {
|
||||||
workflowTimeout = data.workflowData.settings!.executionTimeout as number > 0 && data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
workflowTimeout = data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowTimeout) {
|
const processTimeoutFunction = (timeout: number) => {
|
||||||
const timeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
|
||||||
executionTimeout = setTimeout(() => {
|
|
||||||
this.activeExecutions.stopExecution(executionId, 'timeout');
|
this.activeExecutions.stopExecution(executionId, 'timeout');
|
||||||
|
|
||||||
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
|
executionTimeout = setTimeout(() => subprocess.kill(), Math.max(timeout * 0.2, 5000)); // minimum 5 seconds
|
||||||
}, timeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflowTimeout > 0) {
|
||||||
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number) * 1000; // as seconds
|
||||||
|
// Start timeout already now but give process at least 5 seconds to start.
|
||||||
|
// Without it could would it be possible that the workflow executions times out before it even got started if
|
||||||
|
// the timeout time is very short as the process start time can be quite long.
|
||||||
|
executionTimeout = setTimeout(processTimeoutFunction, Math.max(5000, workflowTimeout), workflowTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of child spawned executions
|
||||||
|
// If after the child process exits we have
|
||||||
|
// outstanding executions, we remove them
|
||||||
|
const childExecutionIds: string[] = [];
|
||||||
|
|
||||||
// Listen to data from the subprocess
|
// Listen to data from the subprocess
|
||||||
subprocess.on('message', async (message: IProcessMessage) => {
|
subprocess.on('message', async (message: IProcessMessage) => {
|
||||||
if (message.type === 'end') {
|
if (message.type === 'start') {
|
||||||
|
// Now that the execution actually started set the timeout again so that does not time out to early.
|
||||||
|
startedAt = new Date();
|
||||||
|
if (workflowTimeout > 0) {
|
||||||
|
clearTimeout(executionTimeout);
|
||||||
|
executionTimeout = setTimeout(processTimeoutFunction, workflowTimeout, workflowTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (message.type === 'end') {
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
this.activeExecutions.remove(executionId!, message.data.runData);
|
this.activeExecutions.remove(executionId!, message.data.runData);
|
||||||
|
|
||||||
|
@ -474,14 +477,20 @@ export class WorkflowRunner {
|
||||||
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
this.processError(timeoutError, startedAt, data.executionMode, executionId);
|
||||||
} else if (message.type === 'startExecution') {
|
} else if (message.type === 'startExecution') {
|
||||||
const executionId = await this.activeExecutions.add(message.data.runData);
|
const executionId = await this.activeExecutions.add(message.data.runData);
|
||||||
|
childExecutionIds.push(executionId);
|
||||||
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
|
subprocess.send({ type: 'executionId', data: {executionId} } as IProcessMessage);
|
||||||
} else if (message.type === 'finishExecution') {
|
} else if (message.type === 'finishExecution') {
|
||||||
|
const executionIdIndex = childExecutionIds.indexOf(message.data.executionId);
|
||||||
|
if (executionIdIndex !== -1) {
|
||||||
|
childExecutionIds.splice(executionIdIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
await this.activeExecutions.remove(message.data.executionId, message.data.result);
|
await this.activeExecutions.remove(message.data.executionId, message.data.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also get informed when the processes does exit especially when it did crash or timed out
|
// Also get informed when the processes does exit especially when it did crash or timed out
|
||||||
subprocess.on('exit', (code, signal) => {
|
subprocess.on('exit', async (code, signal) => {
|
||||||
if (signal === 'SIGTERM'){
|
if (signal === 'SIGTERM'){
|
||||||
// Execution timed out and its process has been terminated
|
// Execution timed out and its process has been terminated
|
||||||
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
const timeoutError = new WorkflowOperationError('Workflow execution timed out!');
|
||||||
|
@ -493,6 +502,17 @@ export class WorkflowRunner {
|
||||||
|
|
||||||
this.processError(executionError, startedAt, data.executionMode, executionId);
|
this.processError(executionError, startedAt, data.executionMode, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const executionId of childExecutionIds) {
|
||||||
|
// When the child process exits, if we still have
|
||||||
|
// pending child executions, we mark them as finished
|
||||||
|
// They will display as unknown to the user
|
||||||
|
// Instead of pending forever as executing when it
|
||||||
|
// actually isn't anymore.
|
||||||
|
await this.activeExecutions.remove(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
clearTimeout(executionTimeout);
|
clearTimeout(executionTimeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
|
IWorkflowExecuteProcess,
|
||||||
IWorkflowExecutionDataProcessWithExecution,
|
IWorkflowExecutionDataProcessWithExecution,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
WorkflowExecuteAdditionalData,
|
WorkflowExecuteAdditionalData,
|
||||||
|
@ -40,6 +41,9 @@ export class WorkflowRunnerProcess {
|
||||||
workflow: Workflow | undefined;
|
workflow: Workflow | undefined;
|
||||||
workflowExecute: WorkflowExecute | undefined;
|
workflowExecute: WorkflowExecute | undefined;
|
||||||
executionIdCallback: (executionId: string) => void | undefined;
|
executionIdCallback: (executionId: string) => void | undefined;
|
||||||
|
childExecutions: {
|
||||||
|
[key: string]: IWorkflowExecuteProcess,
|
||||||
|
} = {};
|
||||||
|
|
||||||
static async stopProcess() {
|
static async stopProcess() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -107,8 +111,18 @@ export class WorkflowRunnerProcess {
|
||||||
await Db.init();
|
await Db.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start timeout for the execution
|
||||||
|
let workflowTimeout = config.get('executions.timeout') as number; // initialize with default
|
||||||
|
if (this.data.workflowData.settings && this.data.workflowData.settings.executionTimeout) {
|
||||||
|
workflowTimeout = this.data.workflowData.settings!.executionTimeout as number; // preference on workflow setting
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowTimeout > 0) {
|
||||||
|
workflowTimeout = Math.min(workflowTimeout, config.get('executions.maxTimeout') as number);
|
||||||
|
}
|
||||||
|
|
||||||
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
|
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings });
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
|
||||||
additionalData.hooks = this.getProcessForwardHooks();
|
additionalData.hooks = this.getProcessForwardHooks();
|
||||||
|
|
||||||
const executeWorkflowFunction = additionalData.executeWorkflow;
|
const executeWorkflowFunction = additionalData.executeWorkflow;
|
||||||
|
@ -123,15 +137,21 @@ export class WorkflowRunnerProcess {
|
||||||
});
|
});
|
||||||
let result: IRun;
|
let result: IRun;
|
||||||
try {
|
try {
|
||||||
result = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData);
|
const executeWorkflowFunctionOutput = await executeWorkflowFunction(workflowInfo, additionalData, inputData, executionId, workflowData, runData) as {workflowExecute: WorkflowExecute, workflow: Workflow} as IWorkflowExecuteProcess;
|
||||||
|
const workflowExecute = executeWorkflowFunctionOutput.workflowExecute;
|
||||||
|
this.childExecutions[executionId] = executeWorkflowFunctionOutput;
|
||||||
|
const workflow = executeWorkflowFunctionOutput.workflow;
|
||||||
|
result = await workflowExecute.processRunExecutionData(workflow) as IRun;
|
||||||
|
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
||||||
|
await sendToParentProcess('finishExecution', { executionId, result });
|
||||||
|
delete this.childExecutions[executionId];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await sendToParentProcess('finishExecution', { executionId });
|
await sendToParentProcess('finishExecution', { executionId });
|
||||||
|
delete this.childExecutions[executionId];
|
||||||
// Throw same error we had
|
// Throw same error we had
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToParentProcess('finishExecution', { executionId, result });
|
|
||||||
|
|
||||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
|
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);
|
||||||
return returnData!.data!.main;
|
return returnData!.data!.main;
|
||||||
};
|
};
|
||||||
|
@ -254,6 +274,8 @@ const workflowRunner = new WorkflowRunnerProcess();
|
||||||
process.on('message', async (message: IProcessMessage) => {
|
process.on('message', async (message: IProcessMessage) => {
|
||||||
try {
|
try {
|
||||||
if (message.type === 'startWorkflow') {
|
if (message.type === 'startWorkflow') {
|
||||||
|
await sendToParentProcess('start', {});
|
||||||
|
|
||||||
const runData = await workflowRunner.runWorkflow(message.data);
|
const runData = await workflowRunner.runWorkflow(message.data);
|
||||||
|
|
||||||
await sendToParentProcess('end', {
|
await sendToParentProcess('end', {
|
||||||
|
@ -267,6 +289,18 @@ process.on('message', async (message: IProcessMessage) => {
|
||||||
let runData: IRun;
|
let runData: IRun;
|
||||||
|
|
||||||
if (workflowRunner.workflowExecute !== undefined) {
|
if (workflowRunner.workflowExecute !== undefined) {
|
||||||
|
|
||||||
|
const executionIds = Object.keys(workflowRunner.childExecutions);
|
||||||
|
|
||||||
|
for (const executionId of executionIds) {
|
||||||
|
const childWorkflowExecute = workflowRunner.childExecutions[executionId];
|
||||||
|
runData = childWorkflowExecute.workflowExecute.getFullRunData(workflowRunner.childExecutions[executionId].startedAt);
|
||||||
|
const timeOutError = message.type === 'timeout' ? new WorkflowOperationError('Workflow execution timed out!') : undefined;
|
||||||
|
|
||||||
|
// If there is any data send it to parent process, if execution timedout add the error
|
||||||
|
await childWorkflowExecute.workflowExecute.processSuccessExecution(workflowRunner.childExecutions[executionId].startedAt, childWorkflowExecute.workflow, timeOutError);
|
||||||
|
}
|
||||||
|
|
||||||
// Workflow started already executing
|
// Workflow started already executing
|
||||||
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);
|
||||||
|
|
||||||
|
|
|
@ -550,6 +550,10 @@ export class WorkflowExecute {
|
||||||
executionLoop:
|
executionLoop:
|
||||||
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
|
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {
|
||||||
|
|
||||||
|
if (this.additionalData.executionTimeoutTimestamp !== undefined && Date.now() >= this.additionalData.executionTimeoutTimestamp) {
|
||||||
|
gotCancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (gotCancel === true) {
|
if (gotCancel === true) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
|
@ -748,6 +748,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
webhookBaseUrl: string;
|
webhookBaseUrl: string;
|
||||||
webhookTestBaseUrl: string;
|
webhookTestBaseUrl: string;
|
||||||
currentNodeParameters?: INodeParameters;
|
currentNodeParameters?: INodeParameters;
|
||||||
|
executionTimeoutTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowExecuteMode = 'cli' | 'error' | 'integrated' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook';
|
export type WorkflowExecuteMode = 'cli' | 'error' | 'integrated' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook';
|
||||||
|
|
Loading…
Reference in a new issue