mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
refactor: Make execution IDs mandatory in BE (#8299)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
parent
0f4f472a72
commit
e1acb5911a
|
@ -49,6 +49,7 @@ export class ActiveExecutions {
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
workflowData: executionData.workflowData,
|
workflowData: executionData.workflowData,
|
||||||
status: executionStatus,
|
status: executionStatus,
|
||||||
|
workflowId: executionData.workflowData.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (executionData.retryOf !== undefined) {
|
if (executionData.retryOf !== undefined) {
|
||||||
|
|
|
@ -81,7 +81,6 @@ export type ITagWithCountDb = Pick<TagEntity, 'id' | 'name' | 'createdAt' | 'upd
|
||||||
|
|
||||||
// Almost identical to editor-ui.Interfaces.ts
|
// Almost identical to editor-ui.Interfaces.ts
|
||||||
export interface IWorkflowDb extends IWorkflowBase {
|
export interface IWorkflowDb extends IWorkflowBase {
|
||||||
id: string;
|
|
||||||
tags?: TagEntity[];
|
tags?: TagEntity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,18 +118,18 @@ export interface IExecutionBase {
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
stoppedAt?: Date; // empty value means execution is still running
|
stoppedAt?: Date; // empty value means execution is still running
|
||||||
workflowId?: string; // To be able to filter executions easily //
|
workflowId: string;
|
||||||
finished: boolean;
|
finished: boolean;
|
||||||
retryOf?: string; // If it is a retry, the id of the execution it is a retry of.
|
retryOf?: string; // If it is a retry, the id of the execution it is a retry of.
|
||||||
retrySuccessId?: string; // If it failed and a retry did succeed. The id of the successful retry.
|
retrySuccessId?: string; // If it failed and a retry did succeed. The id of the successful retry.
|
||||||
status: ExecutionStatus;
|
status: ExecutionStatus;
|
||||||
|
waitTill?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data in regular format with references
|
// Data in regular format with references
|
||||||
export interface IExecutionDb extends IExecutionBase {
|
export interface IExecutionDb extends IExecutionBase {
|
||||||
data: IRunExecutionData;
|
data: IRunExecutionData;
|
||||||
waitTill?: Date | null;
|
workflowData: IWorkflowBase;
|
||||||
workflowData?: IWorkflowBase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,7 +147,6 @@ export interface IExecutionResponse extends IExecutionBase {
|
||||||
data: IRunExecutionData;
|
data: IRunExecutionData;
|
||||||
retryOf?: string;
|
retryOf?: string;
|
||||||
retrySuccessId?: string;
|
retrySuccessId?: string;
|
||||||
waitTill?: Date | null;
|
|
||||||
workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials;
|
workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,9 +160,7 @@ export interface IExecutionFlatted extends IExecutionBase {
|
||||||
export interface IExecutionFlattedDb extends IExecutionBase {
|
export interface IExecutionFlattedDb extends IExecutionBase {
|
||||||
id: string;
|
id: string;
|
||||||
data: string;
|
data: string;
|
||||||
waitTill?: Date | null;
|
|
||||||
workflowData: Omit<IWorkflowBase, 'pinData'>;
|
workflowData: Omit<IWorkflowBase, 'pinData'>;
|
||||||
status: ExecutionStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionFlattedResponse extends IExecutionFlatted {
|
export interface IExecutionFlattedResponse extends IExecutionFlatted {
|
||||||
|
|
|
@ -33,7 +33,7 @@ export = {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Container.get(ExecutionRepository).hardDelete({
|
await Container.get(ExecutionRepository).hardDelete({
|
||||||
workflowId: execution.workflowId as string,
|
workflowId: execution.workflowId,
|
||||||
executionId: execution.id,
|
executionId: execution.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { parse, stringify } from 'flatted';
|
|
||||||
import picocolors from 'picocolors';
|
import picocolors from 'picocolors';
|
||||||
import {
|
import {
|
||||||
ErrorReporterProxy as ErrorReporter,
|
ErrorReporterProxy as ErrorReporter,
|
||||||
|
@ -8,13 +7,7 @@ import {
|
||||||
NodeApiError,
|
NodeApiError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import type {
|
|
||||||
IExecutionDb,
|
|
||||||
IExecutionFlatted,
|
|
||||||
IExecutionFlattedDb,
|
|
||||||
IExecutionResponse,
|
|
||||||
IWorkflowDb,
|
|
||||||
} from '@/Interfaces';
|
|
||||||
import { inDevelopment } from '@/constants';
|
import { inDevelopment } from '@/constants';
|
||||||
import { ResponseError } from './errors/response-errors/abstract/response.error';
|
import { ResponseError } from './errors/response-errors/abstract/response.error';
|
||||||
|
|
||||||
|
@ -173,68 +166,6 @@ export function send<T, R extends Request, S extends Response>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Flattens the Execution data.
|
|
||||||
* As it contains a lot of references which normally would be saved as duplicate data
|
|
||||||
* with regular JSON.stringify it gets flattened which keeps the references in place.
|
|
||||||
*
|
|
||||||
* @param {IExecutionDb} fullExecutionData The data to flatten
|
|
||||||
*/
|
|
||||||
// TODO: Remove this functions since it's purpose should be fulfilled by the execution repository
|
|
||||||
export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted {
|
|
||||||
// Flatten the data
|
|
||||||
const returnData: IExecutionFlatted = {
|
|
||||||
data: stringify(fullExecutionData.data),
|
|
||||||
mode: fullExecutionData.mode,
|
|
||||||
// @ts-ignore
|
|
||||||
waitTill: fullExecutionData.waitTill,
|
|
||||||
startedAt: fullExecutionData.startedAt,
|
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
|
||||||
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
|
||||||
workflowId: fullExecutionData.workflowId,
|
|
||||||
|
|
||||||
workflowData: fullExecutionData.workflowData!,
|
|
||||||
status: fullExecutionData.status,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fullExecutionData.id !== undefined) {
|
|
||||||
returnData.id = fullExecutionData.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullExecutionData.retryOf !== undefined) {
|
|
||||||
returnData.retryOf = fullExecutionData.retryOf.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullExecutionData.retrySuccessId !== undefined) {
|
|
||||||
returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unflattens the Execution data.
|
|
||||||
*
|
|
||||||
* @param {IExecutionFlattedDb} fullExecutionData The data to unflatten
|
|
||||||
*/
|
|
||||||
// TODO: Remove this functions since it's purpose should be fulfilled by the execution repository
|
|
||||||
export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse {
|
|
||||||
const returnData: IExecutionResponse = {
|
|
||||||
id: fullExecutionData.id,
|
|
||||||
workflowData: fullExecutionData.workflowData as IWorkflowDb,
|
|
||||||
data: parse(fullExecutionData.data),
|
|
||||||
mode: fullExecutionData.mode,
|
|
||||||
waitTill: fullExecutionData.waitTill ? fullExecutionData.waitTill : undefined,
|
|
||||||
startedAt: fullExecutionData.startedAt,
|
|
||||||
stoppedAt: fullExecutionData.stoppedAt,
|
|
||||||
finished: fullExecutionData.finished ? fullExecutionData.finished : false,
|
|
||||||
workflowId: fullExecutionData.workflowId,
|
|
||||||
status: fullExecutionData.status,
|
|
||||||
};
|
|
||||||
|
|
||||||
return returnData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flattenObject = (obj: { [x: string]: any }, prefix = '') =>
|
export const flattenObject = (obj: { [x: string]: any }, prefix = '') =>
|
||||||
Object.keys(obj).reduce((acc, k) => {
|
Object.keys(obj).reduce((acc, k) => {
|
||||||
const pre = prefix.length ? prefix + '.' : '';
|
const pre = prefix.length ? prefix + '.' : '';
|
||||||
|
|
|
@ -4,14 +4,8 @@ import {
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import type { IExecutionsStopData, IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||||
import type {
|
|
||||||
IExecutionResponse,
|
|
||||||
IExecutionsStopData,
|
|
||||||
IWorkflowExecutionDataProcess,
|
|
||||||
} from '@/Interfaces';
|
|
||||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||||
import { recoverExecutionDataFromEventLogMessages } from './eventbus/MessageEventBus/recoverEvents';
|
|
||||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||||
import { OwnershipService } from './services/ownership.service';
|
import { OwnershipService } from './services/ownership.service';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
|
@ -79,42 +73,22 @@ export class WaitTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check in database
|
// Also check in database
|
||||||
const execution = await this.executionRepository.findSingleExecution(executionId, {
|
const fullExecutionData = await this.executionRepository.findSingleExecution(executionId, {
|
||||||
includeData: true,
|
includeData: true,
|
||||||
|
unflattenData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!execution) {
|
if (!fullExecutionData) {
|
||||||
throw new ApplicationError('Execution not found.', {
|
throw new ApplicationError('Execution not found.', {
|
||||||
extra: { executionId },
|
extra: { executionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['new', 'unknown', 'waiting', 'running'].includes(execution.status)) {
|
if (!['new', 'unknown', 'waiting', 'running'].includes(fullExecutionData.status)) {
|
||||||
throw new WorkflowOperationError(
|
throw new WorkflowOperationError(
|
||||||
`Only running or waiting executions can be stopped and ${executionId} is currently ${execution.status}.`,
|
`Only running or waiting executions can be stopped and ${executionId} is currently ${fullExecutionData.status}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let fullExecutionData: IExecutionResponse;
|
|
||||||
try {
|
|
||||||
fullExecutionData = ResponseHelper.unflattenExecutionData(execution);
|
|
||||||
} catch (error) {
|
|
||||||
// if the execution ended in an unforseen, non-cancelable state, try to recover it
|
|
||||||
await recoverExecutionDataFromEventLogMessages(executionId, [], true);
|
|
||||||
// find recovered data
|
|
||||||
const restoredExecution = await Container.get(ExecutionRepository).findSingleExecution(
|
|
||||||
executionId,
|
|
||||||
{
|
|
||||||
includeData: true,
|
|
||||||
unflattenData: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!restoredExecution) {
|
|
||||||
throw new ApplicationError('Execution could not be recovered or canceled.', {
|
|
||||||
extra: { executionId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fullExecutionData = restoredExecution;
|
|
||||||
}
|
|
||||||
// Set in execution in DB as failed and remove waitTill time
|
// Set in execution in DB as failed and remove waitTill time
|
||||||
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
|
const error = new WorkflowOperationError('Workflow-Execution has been canceled!');
|
||||||
|
|
||||||
|
@ -184,4 +158,11 @@ export class WaitTracker {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
clearInterval(this.mainTimer);
|
||||||
|
Object.keys(this.waitingExecutions).forEach((executionId) => {
|
||||||
|
clearTimeout(this.waitingExecutions[executionId].timer);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ export class WaitingWebhooks implements IWebhookManager {
|
||||||
const { workflowData } = execution;
|
const { workflowData } = execution;
|
||||||
|
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: workflowData.id!,
|
id: workflowData.id,
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
|
@ -90,7 +90,7 @@ export class WaitingWebhooks implements IWebhookManager {
|
||||||
|
|
||||||
let workflowOwner;
|
let workflowOwner;
|
||||||
try {
|
try {
|
||||||
workflowOwner = await this.ownershipService.getWorkflowOwnerCached(workflowData.id!);
|
workflowOwner = await this.ownershipService.getWorkflowOwnerCached(workflowData.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NotFoundError('Could not find workflow');
|
throw new NotFoundError('Could not find workflow');
|
||||||
}
|
}
|
||||||
|
|
|
@ -420,7 +420,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
// Workflow is saved so update in database
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
await Container.get(WorkflowStaticDataService).saveStaticDataById(
|
await Container.get(WorkflowStaticDataService).saveStaticDataById(
|
||||||
this.workflowData.id as string,
|
this.workflowData.id,
|
||||||
newStaticData,
|
newStaticData,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -464,7 +464,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
this.retryOf,
|
this.retryOf,
|
||||||
);
|
);
|
||||||
await Container.get(ExecutionRepository).hardDelete({
|
await Container.get(ExecutionRepository).hardDelete({
|
||||||
workflowId: this.workflowData.id as string,
|
workflowId: this.workflowData.id,
|
||||||
executionId: this.executionId,
|
executionId: this.executionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -483,7 +483,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
|
|
||||||
await updateExistingExecution({
|
await updateExistingExecution({
|
||||||
executionId: this.executionId,
|
executionId: this.executionId,
|
||||||
workflowId: this.workflowData.id as string,
|
workflowId: this.workflowData.id,
|
||||||
executionData: fullExecutionData,
|
executionData: fullExecutionData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -566,7 +566,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
// Workflow is saved so update in database
|
// Workflow is saved so update in database
|
||||||
try {
|
try {
|
||||||
await Container.get(WorkflowStaticDataService).saveStaticDataById(
|
await Container.get(WorkflowStaticDataService).saveStaticDataById(
|
||||||
this.workflowData.id as string,
|
this.workflowData.id,
|
||||||
newStaticData,
|
newStaticData,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -601,7 +601,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
|
|
||||||
await updateExistingExecution({
|
await updateExistingExecution({
|
||||||
executionId: this.executionId,
|
executionId: this.executionId,
|
||||||
workflowId: this.workflowData.id as string,
|
workflowId: this.workflowData.id,
|
||||||
executionData: fullExecutionData,
|
executionData: fullExecutionData,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -702,7 +702,7 @@ export async function getRunData(
|
||||||
|
|
||||||
export async function getWorkflowData(
|
export async function getWorkflowData(
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
parentWorkflowId?: string,
|
parentWorkflowId: string,
|
||||||
parentWorkflowSettings?: IWorkflowSettings,
|
parentWorkflowSettings?: IWorkflowSettings,
|
||||||
): Promise<IWorkflowBase> {
|
): Promise<IWorkflowBase> {
|
||||||
if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
|
if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
|
||||||
|
@ -748,7 +748,7 @@ async function executeWorkflow(
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
options: {
|
options: {
|
||||||
node?: INode;
|
node?: INode;
|
||||||
parentWorkflowId?: string;
|
parentWorkflowId: string;
|
||||||
inputData?: INodeExecutionData[];
|
inputData?: INodeExecutionData[];
|
||||||
parentExecutionId?: string;
|
parentExecutionId?: string;
|
||||||
loadedWorkflowData?: IWorkflowBase;
|
loadedWorkflowData?: IWorkflowBase;
|
||||||
|
@ -769,7 +769,7 @@ async function executeWorkflow(
|
||||||
|
|
||||||
const workflowName = workflowData ? workflowData.name : undefined;
|
const workflowName = workflowData ? workflowData.name : undefined;
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: workflowData.id?.toString(),
|
id: workflowData.id,
|
||||||
name: workflowName,
|
name: workflowName,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
|
@ -788,10 +788,7 @@ async function executeWorkflow(
|
||||||
if (options.parentExecutionId !== undefined) {
|
if (options.parentExecutionId !== undefined) {
|
||||||
executionId = options.parentExecutionId;
|
executionId = options.parentExecutionId;
|
||||||
} else {
|
} else {
|
||||||
executionId =
|
executionId = options.parentExecutionId ?? (await activeExecutions.add(runData));
|
||||||
options.parentExecutionId !== undefined
|
|
||||||
? options.parentExecutionId
|
|
||||||
: await activeExecutions.add(runData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void internalHooks.onWorkflowBeforeExecute(executionId || '', runData);
|
void internalHooks.onWorkflowBeforeExecute(executionId || '', runData);
|
||||||
|
@ -801,7 +798,7 @@ async function executeWorkflow(
|
||||||
await PermissionChecker.check(workflow, additionalData.userId);
|
await PermissionChecker.check(workflow, additionalData.userId);
|
||||||
await PermissionChecker.checkSubworkflowExecutePolicy(
|
await PermissionChecker.checkSubworkflowExecutePolicy(
|
||||||
workflow,
|
workflow,
|
||||||
options.parentWorkflowId!,
|
options.parentWorkflowId,
|
||||||
options.node,
|
options.node,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -879,6 +876,7 @@ async function executeWorkflow(
|
||||||
stoppedAt: fullRunData.stoppedAt,
|
stoppedAt: fullRunData.stoppedAt,
|
||||||
status: fullRunData.status,
|
status: fullRunData.status,
|
||||||
workflowData,
|
workflowData,
|
||||||
|
workflowId: workflowData.id,
|
||||||
};
|
};
|
||||||
if (workflowData.id) {
|
if (workflowData.id) {
|
||||||
fullExecutionData.workflowId = workflowData.id;
|
fullExecutionData.workflowId = workflowData.id;
|
||||||
|
@ -1082,7 +1080,7 @@ export function getWorkflowHooksWorkerMain(
|
||||||
|
|
||||||
if (shouldNotSave) {
|
if (shouldNotSave) {
|
||||||
await Container.get(ExecutionRepository).hardDelete({
|
await Container.get(ExecutionRepository).hardDelete({
|
||||||
workflowId: this.workflowData.id as string,
|
workflowId: this.workflowData.id,
|
||||||
executionId: this.executionId,
|
executionId: this.executionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,16 +197,16 @@ class WorkflowRunnerProcess {
|
||||||
additionalData.executeWorkflow = async (
|
additionalData.executeWorkflow = async (
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
options?: {
|
options: {
|
||||||
parentWorkflowId?: string;
|
parentWorkflowId: string;
|
||||||
inputData?: INodeExecutionData[];
|
inputData?: INodeExecutionData[];
|
||||||
parentWorkflowSettings?: IWorkflowSettings;
|
parentWorkflowSettings?: IWorkflowSettings;
|
||||||
},
|
},
|
||||||
): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
): Promise<Array<INodeExecutionData[] | null> | IRun> => {
|
||||||
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(
|
const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(
|
||||||
workflowInfo,
|
workflowInfo,
|
||||||
options?.parentWorkflowId,
|
options.parentWorkflowId,
|
||||||
options?.parentWorkflowSettings,
|
options.parentWorkflowSettings,
|
||||||
);
|
);
|
||||||
const runData = await WorkflowExecuteAdditionalData.getRunData(
|
const runData = await WorkflowExecuteAdditionalData.getRunData(
|
||||||
workflowData,
|
workflowData,
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { UrlService } from '@/services/url.service';
|
||||||
import { SettingsRepository } from '@db/repositories/settings.repository';
|
import { SettingsRepository } from '@db/repositories/settings.repository';
|
||||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||||
|
import { WaitTracker } from '@/WaitTracker';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
|
@ -99,6 +100,8 @@ export class Start extends BaseCommand {
|
||||||
// Stop with trying to activate workflows that could not be activated
|
// Stop with trying to activate workflows that could not be activated
|
||||||
this.activeWorkflowRunner.removeAllQueuedWorkflowActivations();
|
this.activeWorkflowRunner.removeAllQueuedWorkflowActivations();
|
||||||
|
|
||||||
|
Container.get(WaitTracker).shutdown();
|
||||||
|
|
||||||
await this.externalHooks?.run('n8n.stop', []);
|
await this.externalHooks?.run('n8n.stop', []);
|
||||||
|
|
||||||
if (Container.get(MultiMainSetup).isEnabled) {
|
if (Container.get(MultiMainSetup).isEnabled) {
|
||||||
|
|
|
@ -122,7 +122,7 @@ export class Worker extends BaseCommand {
|
||||||
{ extra: { executionId } },
|
{ extra: { executionId } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const workflowId = fullExecutionData.workflowData.id!; // @tech_debt Ensure this is not optional
|
const workflowId = fullExecutionData.workflowData.id;
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Start job: ${job.id} (Workflow ID: ${workflowId} | Execution: ${executionId})`,
|
`Start job: ${job.id} (Workflow ID: ${workflowId} | Execution: ${executionId})`,
|
||||||
|
|
|
@ -762,6 +762,12 @@ export const schema = {
|
||||||
default: '',
|
default: '',
|
||||||
env: 'N8N_USER_MANAGEMENT_JWT_SECRET',
|
env: 'N8N_USER_MANAGEMENT_JWT_SECRET',
|
||||||
},
|
},
|
||||||
|
jwtDuration: {
|
||||||
|
doc: 'Set a specific JWT secret (optional - n8n can generate one)', // Generated @ start.ts
|
||||||
|
format: Number,
|
||||||
|
default: 168,
|
||||||
|
env: 'N8N_USER_MANAGEMENT_JWT_DURATION',
|
||||||
|
},
|
||||||
isInstanceOwnerSetUp: {
|
isInstanceOwnerSetUp: {
|
||||||
// n8n loads this setting from DB on startup
|
// n8n loads this setting from DB on startup
|
||||||
doc: "Whether the instance owner's account has been set up",
|
doc: "Whether the instance owner's account has been set up",
|
||||||
|
|
|
@ -229,7 +229,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
const { connections, nodes, name, settings } = workflowData ?? {};
|
const { connections, nodes, name, settings } = workflowData ?? {};
|
||||||
await this.executionDataRepository.insert({
|
await this.executionDataRepository.insert({
|
||||||
executionId,
|
executionId,
|
||||||
workflowData: { connections, nodes, name, settings, id: workflowData?.id },
|
workflowData: { connections, nodes, name, settings, id: workflowData.id },
|
||||||
data: stringify(data),
|
data: stringify(data),
|
||||||
});
|
});
|
||||||
return String(executionId);
|
return String(executionId);
|
||||||
|
|
|
@ -54,6 +54,7 @@ export function prepareExecutionDataForDbUpdate(parameters: {
|
||||||
workflowData: pristineWorkflowData,
|
workflowData: pristineWorkflowData,
|
||||||
waitTill: runData.waitTill,
|
waitTill: runData.waitTill,
|
||||||
status: workflowStatusFinal,
|
status: workflowStatusFinal,
|
||||||
|
workflowId: pristineWorkflowData.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (retryOf !== undefined) {
|
if (retryOf !== undefined) {
|
||||||
|
|
|
@ -281,7 +281,7 @@ export class ExecutionsService {
|
||||||
if (req.body.loadWorkflow) {
|
if (req.body.loadWorkflow) {
|
||||||
// Loads the currently saved workflow to execute instead of the
|
// Loads the currently saved workflow to execute instead of the
|
||||||
// one saved at the time of the execution.
|
// one saved at the time of the execution.
|
||||||
const workflowId = execution.workflowData.id as string;
|
const workflowId = execution.workflowData.id;
|
||||||
const workflowData = (await Container.get(WorkflowRepository).findOneBy({
|
const workflowData = (await Container.get(WorkflowRepository).findOneBy({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
})) as IWorkflowBase;
|
})) as IWorkflowBase;
|
||||||
|
@ -296,7 +296,7 @@ export class ExecutionsService {
|
||||||
data.workflowData = workflowData;
|
data.workflowData = workflowData;
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
const nodeTypes = Container.get(NodeTypes);
|
||||||
const workflowInstance = new Workflow({
|
const workflowInstance = new Workflow({
|
||||||
id: workflowData.id as string,
|
id: workflowData.id,
|
||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
nodes: workflowData.nodes,
|
nodes: workflowData.nodes,
|
||||||
connections: workflowData.connections,
|
connections: workflowData.connections,
|
||||||
|
|
83
packages/cli/test/unit/WaitTracker.test.ts
Normal file
83
packages/cli/test/unit/WaitTracker.test.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { WaitTracker } from '@/WaitTracker';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
|
import type { IExecutionResponse } from '@/Interfaces';
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe('WaitTracker', () => {
|
||||||
|
const executionRepository = mock<ExecutionRepository>();
|
||||||
|
|
||||||
|
const execution = mock<IExecutionResponse>({
|
||||||
|
id: '123',
|
||||||
|
waitTill: new Date(Date.now() + 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor()', () => {
|
||||||
|
it('should query DB for waiting executions', async () => {
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||||
|
|
||||||
|
new WaitTracker(mock(), executionRepository, mock());
|
||||||
|
|
||||||
|
expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if no executions to start, should do nothing', () => {
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||||
|
|
||||||
|
new WaitTracker(mock(), executionRepository, mock());
|
||||||
|
|
||||||
|
expect(executionRepository.findSingleExecution).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if execution to start', () => {
|
||||||
|
it('if not enough time passed, should not start execution', async () => {
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||||
|
const waitTracker = new WaitTracker(mock(), executionRepository, mock());
|
||||||
|
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||||
|
await waitTracker.getWaitingExecutions();
|
||||||
|
|
||||||
|
const startExecutionSpy = jest.spyOn(waitTracker, 'startExecution');
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
|
||||||
|
expect(startExecutionSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if enough time passed, should start execution', async () => {
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||||
|
const waitTracker = new WaitTracker(mock(), executionRepository, mock());
|
||||||
|
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([execution]);
|
||||||
|
await waitTracker.getWaitingExecutions();
|
||||||
|
|
||||||
|
const startExecutionSpy = jest.spyOn(waitTracker, 'startExecution');
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(2_000);
|
||||||
|
|
||||||
|
expect(startExecutionSpy).toHaveBeenCalledWith(execution.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startExecution()', () => {
|
||||||
|
it('should query for execution to start', async () => {
|
||||||
|
executionRepository.getWaitingExecutions.mockResolvedValue([]);
|
||||||
|
const waitTracker = new WaitTracker(mock(), executionRepository, mock());
|
||||||
|
|
||||||
|
executionRepository.findSingleExecution.mockResolvedValue(execution);
|
||||||
|
waitTracker.startExecution(execution.id);
|
||||||
|
jest.advanceTimersByTime(5);
|
||||||
|
|
||||||
|
expect(executionRepository.findSingleExecution).toHaveBeenCalledWith(execution.id, {
|
||||||
|
includeData: true,
|
||||||
|
unflattenData: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1884,7 +1884,7 @@ export interface IWaitingForExecutionSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowBase {
|
export interface IWorkflowBase {
|
||||||
id?: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
@ -1921,7 +1921,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
options: {
|
options: {
|
||||||
node?: INode;
|
node?: INode;
|
||||||
parentWorkflowId?: string;
|
parentWorkflowId: string;
|
||||||
inputData?: INodeExecutionData[];
|
inputData?: INodeExecutionData[];
|
||||||
parentExecutionId?: string;
|
parentExecutionId?: string;
|
||||||
loadedWorkflowData?: IWorkflowBase;
|
loadedWorkflowData?: IWorkflowBase;
|
||||||
|
|
Loading…
Reference in a new issue