13227 workflow wrong completed workflowrun state when multiple branches (#13344)
This commit is contained in:
@ -6,6 +6,7 @@ export const getNodeVariantFromStepRunStatus = (
|
|||||||
): WorkflowDiagramNodeVariant => {
|
): WorkflowDiagramNodeVariant => {
|
||||||
switch (runStatus) {
|
switch (runStatus) {
|
||||||
case 'SUCCESS':
|
case 'SUCCESS':
|
||||||
|
case 'STOPPED':
|
||||||
return 'success';
|
return 'success';
|
||||||
case 'FAILED':
|
case 'FAILED':
|
||||||
return 'failure';
|
return 'failure';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import {
|
|||||||
WorkflowVersionStepException,
|
WorkflowVersionStepException,
|
||||||
WorkflowVersionStepExceptionCode,
|
WorkflowVersionStepExceptionCode,
|
||||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
||||||
import { StepOutput } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
|
||||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
|
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
|
||||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||||
@ -329,18 +328,14 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newStepOutput: StepOutput = {
|
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||||
id: stepId,
|
stepId,
|
||||||
output: {
|
stepInfo: {
|
||||||
|
status: StepStatus.SUCCESS,
|
||||||
result: enrichedResponse,
|
result: enrichedResponse,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
stepOutput: newStepOutput,
|
|
||||||
stepStatus: StepStatus.SUCCESS,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.workflowRunnerWorkspaceService.resume({
|
await this.workflowRunnerWorkspaceService.resume({
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export type WorkflowExecutorInput = {
|
|||||||
stepIds: string[];
|
stepIds: string[];
|
||||||
workflowRunId: string;
|
workflowRunId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
shouldComputeWorkflowRunStatus?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowBranchExecutorInput = {
|
export type WorkflowBranchExecutorInput = {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
WorkflowActionType,
|
WorkflowActionType,
|
||||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||||
|
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
|
|
||||||
describe('canExecuteStep', () => {
|
describe('canExecuteStep', () => {
|
||||||
const steps = [
|
const steps = [
|
||||||
@ -56,7 +57,12 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = canExecuteStep({ stepInfos, steps, stepId: 'step-3' });
|
const result = canExecuteStep({
|
||||||
|
stepInfos,
|
||||||
|
steps,
|
||||||
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
@ -77,6 +83,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
@ -95,6 +102,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
@ -113,6 +121,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
@ -133,6 +142,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
@ -151,6 +161,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
@ -169,6 +180,7 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
@ -187,7 +199,38 @@ describe('canExecuteStep', () => {
|
|||||||
},
|
},
|
||||||
steps,
|
steps,
|
||||||
stepId: 'step-3',
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false if workflowRun is not RUNNING', () => {
|
||||||
|
const stepInfos = {
|
||||||
|
'step-1': {
|
||||||
|
status: StepStatus.SUCCESS,
|
||||||
|
},
|
||||||
|
'step-2': {
|
||||||
|
status: StepStatus.SUCCESS,
|
||||||
|
},
|
||||||
|
'step-3': {
|
||||||
|
status: StepStatus.NOT_STARTED,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const workflowRunStatus of [
|
||||||
|
WorkflowRunStatus.FAILED,
|
||||||
|
WorkflowRunStatus.ENQUEUED,
|
||||||
|
WorkflowRunStatus.COMPLETED,
|
||||||
|
WorkflowRunStatus.NOT_STARTED,
|
||||||
|
]) {
|
||||||
|
const result = canExecuteStep({
|
||||||
|
stepInfos,
|
||||||
|
steps,
|
||||||
|
stepId: 'step-3',
|
||||||
|
workflowRunStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
|
import { workflowShouldFail } from 'src/modules/workflow/workflow-executor/utils/workflow-should-fail.util';
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
|
||||||
|
describe('workflowShouldFail', () => {
|
||||||
|
it('should return true if a failed step exists', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = { 'step-1': { status: StepStatus.FAILED } };
|
||||||
|
|
||||||
|
expect(workflowShouldFail({ steps, stepInfos })).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if no failed step exists', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = { 'step-1': { status: StepStatus.SUCCESS } };
|
||||||
|
|
||||||
|
expect(workflowShouldFail({ steps, stepInfos })).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
import { workflowShouldKeepRunning } from 'src/modules/workflow/workflow-executor/utils/workflow-should-keep-running.util';
|
||||||
|
|
||||||
|
describe('workflowShouldKeepRunning', () => {
|
||||||
|
describe('should return true if', () => {
|
||||||
|
it('running or pending step exists', () => {
|
||||||
|
for (const testStatus of [StepStatus.PENDING, StepStatus.RUNNING]) {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = { 'step-1': { status: testStatus } };
|
||||||
|
|
||||||
|
expect(workflowShouldKeepRunning({ steps, stepInfos })).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('success step with not started executable children exists', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
nextStepIds: ['step-2'],
|
||||||
|
} as WorkflowAction,
|
||||||
|
{
|
||||||
|
id: 'step-2',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = {
|
||||||
|
'step-1': { status: StepStatus.SUCCESS },
|
||||||
|
'step-2': { status: StepStatus.NOT_STARTED },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(workflowShouldKeepRunning({ steps, stepInfos })).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return false', () => {
|
||||||
|
it('workflow run only have success steps', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = { 'step-1': { status: StepStatus.SUCCESS } };
|
||||||
|
|
||||||
|
expect(workflowShouldKeepRunning({ steps, stepInfos })).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('success step with not executable not started children exists', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
id: 'step-1',
|
||||||
|
nextStepIds: ['step-3'],
|
||||||
|
} as WorkflowAction,
|
||||||
|
{
|
||||||
|
id: 'step-2',
|
||||||
|
nextStepIds: ['step-3'],
|
||||||
|
} as WorkflowAction,
|
||||||
|
{
|
||||||
|
id: 'step-3',
|
||||||
|
} as WorkflowAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepInfos = {
|
||||||
|
'step-1': { status: StepStatus.SUCCESS },
|
||||||
|
'step-2': { status: StepStatus.FAILED },
|
||||||
|
'step-3': { status: StepStatus.NOT_STARTED },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(workflowShouldKeepRunning({ steps, stepInfos })).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -2,16 +2,23 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
|
|
||||||
export const canExecuteStep = ({
|
export const canExecuteStep = ({
|
||||||
stepId,
|
stepId,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
workflowRunStatus,
|
||||||
}: {
|
}: {
|
||||||
steps: WorkflowAction[];
|
steps: WorkflowAction[];
|
||||||
stepInfos: WorkflowRunStepInfos;
|
stepInfos: WorkflowRunStepInfos;
|
||||||
stepId: string;
|
stepId: string;
|
||||||
|
workflowRunStatus: WorkflowRunStatus;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (workflowRunStatus !== WorkflowRunStatus.RUNNING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(stepInfos[stepId]?.status) &&
|
isDefined(stepInfos[stepId]?.status) &&
|
||||||
stepInfos[stepId].status !== StepStatus.NOT_STARTED
|
stepInfos[stepId].status !== StepStatus.NOT_STARTED
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
|
||||||
|
export const workflowShouldFail = ({
|
||||||
|
stepInfos,
|
||||||
|
steps,
|
||||||
|
}: {
|
||||||
|
stepInfos: WorkflowRunStepInfos;
|
||||||
|
steps: WorkflowAction[];
|
||||||
|
}) => {
|
||||||
|
const failedSteps = steps.filter(
|
||||||
|
(step) => stepInfos[step.id]?.status === StepStatus.FAILED,
|
||||||
|
);
|
||||||
|
|
||||||
|
return failedSteps.length > 0;
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
|
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||||
|
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
|
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
|
|
||||||
|
export const workflowShouldKeepRunning = ({
|
||||||
|
stepInfos,
|
||||||
|
steps,
|
||||||
|
}: {
|
||||||
|
stepInfos: WorkflowRunStepInfos;
|
||||||
|
steps: WorkflowAction[];
|
||||||
|
}) => {
|
||||||
|
const runningOrPendingStepExists = steps.some((step) =>
|
||||||
|
[StepStatus.PENDING, StepStatus.RUNNING].includes(
|
||||||
|
stepInfos[step.id]?.status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const successStepWithNotStartedExecutableChildren = steps.some(
|
||||||
|
(step) =>
|
||||||
|
stepInfos[step.id]?.status === StepStatus.SUCCESS &&
|
||||||
|
(step.nextStepIds ?? []).some(
|
||||||
|
(nextStepId) =>
|
||||||
|
stepInfos[nextStepId]?.status === StepStatus.NOT_STARTED &&
|
||||||
|
canExecuteStep({
|
||||||
|
stepId: nextStepId,
|
||||||
|
steps,
|
||||||
|
stepInfos,
|
||||||
|
workflowRunStatus: WorkflowRunStatus.RUNNING,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
runningOrPendingStepExists || successStepWithNotStartedExecutableChildren
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||||
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
|
||||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||||
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
|
||||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
@ -47,9 +46,8 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
|
|
||||||
const mockWorkflowRunWorkspaceService = {
|
const mockWorkflowRunWorkspaceService = {
|
||||||
endWorkflowRun: jest.fn(),
|
endWorkflowRun: jest.fn(),
|
||||||
updateWorkflowRunStepStatus: jest.fn(),
|
updateWorkflowRunStepInfo: jest.fn(),
|
||||||
saveWorkflowRunState: jest.fn(),
|
getWorkflowRunOrFail: jest.fn(),
|
||||||
getWorkflowRun: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockBillingService = {
|
const mockBillingService = {
|
||||||
@ -125,11 +123,14 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
nextStepIds: [],
|
nextStepIds: [],
|
||||||
},
|
},
|
||||||
] as WorkflowAction[];
|
] as WorkflowAction[];
|
||||||
|
|
||||||
const mockStepInfos = {
|
const mockStepInfos = {
|
||||||
trigger: { result: {}, status: StepStatus.SUCCESS },
|
trigger: { result: {}, status: StepStatus.SUCCESS },
|
||||||
|
'step-1': { status: StepStatus.NOT_STARTED },
|
||||||
|
'step-2': { status: StepStatus.NOT_STARTED },
|
||||||
};
|
};
|
||||||
|
|
||||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
|
mockWorkflowRunWorkspaceService.getWorkflowRunOrFail.mockReturnValue({
|
||||||
state: { flow: { steps: mockSteps }, stepInfos: mockStepInfos },
|
state: { flow: { steps: mockSteps }, stepInfos: mockStepInfos },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -168,32 +169,30 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(2);
|
).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledWith({
|
).toHaveBeenNthCalledWith(1, {
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepId: 'step-1',
|
stepId: 'step-1',
|
||||||
|
stepInfo: {
|
||||||
|
status: StepStatus.RUNNING,
|
||||||
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.RUNNING,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(2);
|
).toHaveBeenNthCalledWith(2, {
|
||||||
|
stepId: 'step-1',
|
||||||
expect(
|
stepInfo: {
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
...mockStepResult,
|
||||||
).toHaveBeenCalledWith({
|
status: StepStatus.SUCCESS,
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepOutput: {
|
|
||||||
id: 'step-1',
|
|
||||||
output: mockStepResult,
|
|
||||||
},
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.SUCCESS,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// execute second step
|
// execute second step
|
||||||
@ -216,34 +215,30 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
expect(workspaceEventEmitter.emitCustomBatchEvent).not.toHaveBeenCalled();
|
expect(workspaceEventEmitter.emitCustomBatchEvent).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledWith({
|
).toHaveBeenNthCalledWith(1, {
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepId: 'step-1',
|
stepId: 'step-1',
|
||||||
|
stepInfo: {
|
||||||
|
status: StepStatus.RUNNING,
|
||||||
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.RUNNING,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenNthCalledWith(2, {
|
||||||
|
stepId: 'step-1',
|
||||||
expect(
|
stepInfo: {
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
error: 'Step execution failed',
|
||||||
).toHaveBeenCalledWith({
|
status: StepStatus.FAILED,
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepOutput: {
|
|
||||||
id: 'step-1',
|
|
||||||
output: {
|
|
||||||
error: 'Step execution failed',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.FAILED,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -261,32 +256,29 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledWith({
|
).toHaveBeenNthCalledWith(1, {
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepId: 'step-1',
|
stepId: 'step-1',
|
||||||
|
stepInfo: {
|
||||||
|
status: StepStatus.RUNNING,
|
||||||
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.RUNNING,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenNthCalledWith(2, {
|
||||||
|
stepId: 'step-1',
|
||||||
expect(
|
stepInfo: {
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
status: StepStatus.PENDING,
|
||||||
).toHaveBeenCalledWith({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepOutput: {
|
|
||||||
id: 'step-1',
|
|
||||||
output: mockPendingEvent,
|
|
||||||
},
|
},
|
||||||
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
stepStatus: StepStatus.PENDING,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// No recursive call to execute should happen
|
// No recursive call to execute should happen
|
||||||
@ -295,128 +287,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should continue to next step if continueOnFailure is true', async () => {
|
|
||||||
const stepsWithContinueOnFailure = [
|
|
||||||
{
|
|
||||||
id: 'step-1',
|
|
||||||
type: WorkflowActionType.CODE,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: { value: true },
|
|
||||||
retryOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nextStepIds: ['step-2'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'step-2',
|
|
||||||
type: WorkflowActionType.SEND_EMAIL,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
retryOnFailure: { value: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as WorkflowAction[];
|
|
||||||
|
|
||||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValueOnce({
|
|
||||||
state: {
|
|
||||||
flow: { steps: stepsWithContinueOnFailure },
|
|
||||||
stepInfos: mockStepInfos,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockWorkflowExecutor.execute.mockResolvedValueOnce({
|
|
||||||
error: 'Step execution failed but continue',
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.executeFromSteps({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepIds: ['step-1'],
|
|
||||||
workspaceId: mockWorkspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
|
||||||
).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workflowRunWorkspaceService.updateWorkflowRunStepStatus,
|
|
||||||
).toHaveBeenCalledWith({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepId: 'step-1',
|
|
||||||
workspaceId: 'workspace-id',
|
|
||||||
stepStatus: StepStatus.RUNNING,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
|
||||||
).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
|
||||||
).toHaveBeenCalledWith({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepOutput: {
|
|
||||||
id: 'step-1',
|
|
||||||
output: {
|
|
||||||
error: 'Step execution failed but continue',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workspaceId: 'workspace-id',
|
|
||||||
stepStatus: StepStatus.FAILED,
|
|
||||||
});
|
|
||||||
|
|
||||||
// execute second step
|
|
||||||
expect(workflowActionFactory.get).toHaveBeenCalledWith(
|
|
||||||
WorkflowActionType.SEND_EMAIL,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retry on failure if retryOnFailure is true', async () => {
|
|
||||||
const stepsWithRetryOnFailure = [
|
|
||||||
{
|
|
||||||
id: 'step-1',
|
|
||||||
type: WorkflowActionType.CODE,
|
|
||||||
settings: {
|
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: { value: false },
|
|
||||||
retryOnFailure: { value: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as WorkflowAction[];
|
|
||||||
|
|
||||||
mockWorkflowRunWorkspaceService.getWorkflowRun.mockReturnValue({
|
|
||||||
state: {
|
|
||||||
flow: { steps: stepsWithRetryOnFailure },
|
|
||||||
stepInfos: mockStepInfos,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockWorkflowExecutor.execute.mockResolvedValue({
|
|
||||||
error: 'Step execution failed, will retry',
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.executeFromSteps({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
|
||||||
stepIds: ['step-1'],
|
|
||||||
workspaceId: mockWorkspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
||||||
expect(workflowActionFactory.get).toHaveBeenNthCalledWith(
|
|
||||||
attempt,
|
|
||||||
WorkflowActionType.CODE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(workflowActionFactory.get).not.toHaveBeenCalledWith(
|
|
||||||
WorkflowActionType.SEND_EMAIL,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop when billing validation fails', async () => {
|
it('should stop when billing validation fails', async () => {
|
||||||
mockBillingService.isBillingEnabled.mockReturnValueOnce(true);
|
mockBillingService.isBillingEnabled.mockReturnValueOnce(true);
|
||||||
mockBillingService.canBillMeteredProduct.mockReturnValueOnce(false);
|
mockBillingService.canBillMeteredProduct.mockReturnValueOnce(false);
|
||||||
@ -430,7 +300,7 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
expect(workflowActionFactory.get).toHaveBeenCalledTimes(0);
|
expect(workflowActionFactory.get).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledTimes(1);
|
).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(workflowRunWorkspaceService.endWorkflowRun).toHaveBeenCalledTimes(
|
expect(workflowRunWorkspaceService.endWorkflowRun).toHaveBeenCalledTimes(
|
||||||
@ -438,24 +308,15 @@ describe('WorkflowExecutorWorkspaceService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workflowRunWorkspaceService.saveWorkflowRunState,
|
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
|
||||||
).toHaveBeenCalledWith({
|
).toHaveBeenCalledWith({
|
||||||
workflowRunId: mockWorkflowRunId,
|
stepId: 'step-1',
|
||||||
workspaceId: 'workspace-id',
|
stepInfo: {
|
||||||
stepOutput: {
|
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
||||||
id: 'step-1',
|
status: StepStatus.FAILED,
|
||||||
output: {
|
|
||||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
stepStatus: StepStatus.FAILED,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(workflowRunWorkspaceService.endWorkflowRun).toHaveBeenCalledWith({
|
|
||||||
workflowRunId: mockWorkflowRunId,
|
workflowRunId: mockWorkflowRunId,
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,15 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
|
||||||
|
import { WorkflowRunStepInfo } from 'twenty-shared/src/workflow/types/WorkflowRunStateStepInfos';
|
||||||
|
|
||||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||||
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
|
||||||
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
import {
|
import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
StepOutput,
|
|
||||||
WorkflowRunStatus,
|
|
||||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
|
||||||
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
||||||
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||||
import {
|
import {
|
||||||
@ -22,8 +19,9 @@ import {
|
|||||||
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.util';
|
||||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||||
|
import { workflowShouldKeepRunning } from 'src/modules/workflow/workflow-executor/utils/workflow-should-keep-running.util';
|
||||||
const MAX_RETRIES_ON_FAILURE = 3;
|
import { workflowShouldFail } from 'src/modules/workflow/workflow-executor/utils/workflow-should-fail.util';
|
||||||
|
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkflowExecutorWorkspaceService {
|
export class WorkflowExecutorWorkspaceService {
|
||||||
@ -38,6 +36,7 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
stepIds,
|
stepIds,
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
shouldComputeWorkflowRunStatus = true,
|
||||||
}: WorkflowExecutorInput) {
|
}: WorkflowExecutorInput) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
stepIds.map(async (stepIdToExecute) => {
|
stepIds.map(async (stepIdToExecute) => {
|
||||||
@ -48,187 +47,27 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (shouldComputeWorkflowRunStatus) {
|
||||||
|
await this.computeWorkflowRunStatus({
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeFromStep({
|
private async executeFromStep({
|
||||||
stepId,
|
stepId,
|
||||||
attemptCount = 1,
|
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: WorkflowBranchExecutorInput) {
|
}: WorkflowBranchExecutorInput) {
|
||||||
const workflowRunInfo = await this.getWorkflowRunInfoOrEndWorkflowRun({
|
const workflowRun =
|
||||||
stepId,
|
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(workflowRunInfo)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stepToExecute, steps, stepInfos } = workflowRunInfo;
|
|
||||||
|
|
||||||
if (!canExecuteStep({ stepId, steps, stepInfos })) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkCanBillWorkflowNodeExecution =
|
|
||||||
await this.checkCanBillWorkflowNodeExecutionOrEndWorkflowRun({
|
|
||||||
stepIdToExecute: stepToExecute.id,
|
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!checkCanBillWorkflowNodeExecution) {
|
const stepInfos = workflowRun.state.stepInfos;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowAction = this.workflowActionFactory.get(stepToExecute.type);
|
|
||||||
|
|
||||||
let actionOutput: WorkflowActionOutput;
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.updateWorkflowRunStepStatus({
|
|
||||||
workflowRunId,
|
|
||||||
stepId: stepToExecute.id,
|
|
||||||
workspaceId,
|
|
||||||
stepStatus: StepStatus.RUNNING,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
actionOutput = await workflowAction.execute({
|
|
||||||
currentStepId: stepId,
|
|
||||||
steps,
|
|
||||||
context: getWorkflowRunContext(stepInfos),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
actionOutput = {
|
|
||||||
error: error.message ?? 'Execution result error, no data or error',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!actionOutput.error) {
|
|
||||||
this.sendWorkflowNodeRunEvent(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepOutput: StepOutput = {
|
|
||||||
id: stepToExecute.id,
|
|
||||||
output: actionOutput,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (actionOutput.pendingEvent) {
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
|
||||||
workflowRunId,
|
|
||||||
stepOutput,
|
|
||||||
workspaceId,
|
|
||||||
stepStatus: StepStatus.PENDING,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionOutputSuccess = isDefined(actionOutput.result);
|
|
||||||
|
|
||||||
const isValidActionOutput =
|
|
||||||
actionOutputSuccess ||
|
|
||||||
stepToExecute.settings.errorHandlingOptions.continueOnFailure.value;
|
|
||||||
|
|
||||||
if (isValidActionOutput) {
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
|
||||||
workflowRunId,
|
|
||||||
stepOutput,
|
|
||||||
workspaceId,
|
|
||||||
stepStatus: isDefined(actionOutput.result)
|
|
||||||
? StepStatus.SUCCESS
|
|
||||||
: StepStatus.FAILED,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isDefined(stepToExecute.nextStepIds) ||
|
|
||||||
stepToExecute.nextStepIds.length === 0 ||
|
|
||||||
actionOutput.shouldEndWorkflowRun === true
|
|
||||||
) {
|
|
||||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
status: WorkflowRunStatus.COMPLETED,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.executeFromSteps({
|
|
||||||
stepIds: stepToExecute.nextStepIds,
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
stepToExecute.settings.errorHandlingOptions.retryOnFailure.value &&
|
|
||||||
attemptCount < MAX_RETRIES_ON_FAILURE
|
|
||||||
) {
|
|
||||||
await this.executeFromStep({
|
|
||||||
stepId,
|
|
||||||
attemptCount: attemptCount + 1,
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
|
||||||
workflowRunId,
|
|
||||||
stepOutput,
|
|
||||||
workspaceId,
|
|
||||||
stepStatus: StepStatus.FAILED,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
error: stepOutput.output.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getWorkflowRunInfoOrEndWorkflowRun({
|
|
||||||
stepId,
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
}: {
|
|
||||||
stepId: string;
|
|
||||||
workflowRunId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}) {
|
|
||||||
const workflowRun = await this.workflowRunWorkspaceService.getWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isDefined(workflowRun)) {
|
|
||||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
error: `WorkflowRun ${workflowRunId} not found`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefined(workflowRun?.state)) {
|
|
||||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
error: `WorkflowRun ${workflowRunId} doesn't have any state`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const steps = workflowRun.state.flow.steps;
|
const steps = workflowRun.state.flow.steps;
|
||||||
|
|
||||||
@ -245,11 +84,142 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (
|
||||||
stepToExecute,
|
!canExecuteStep({
|
||||||
steps,
|
stepId,
|
||||||
stepInfos: workflowRun.state.stepInfos,
|
steps,
|
||||||
};
|
stepInfos,
|
||||||
|
workflowRunStatus: workflowRun.status,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionOutput: WorkflowActionOutput;
|
||||||
|
|
||||||
|
if (await this.canBillWorkflowNodeExecution(workspaceId)) {
|
||||||
|
const workflowAction = this.workflowActionFactory.get(stepToExecute.type);
|
||||||
|
|
||||||
|
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||||
|
stepId,
|
||||||
|
stepInfo: {
|
||||||
|
status: StepStatus.RUNNING,
|
||||||
|
},
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
actionOutput = await workflowAction.execute({
|
||||||
|
currentStepId: stepId,
|
||||||
|
steps,
|
||||||
|
context: getWorkflowRunContext(stepInfos),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
actionOutput = {
|
||||||
|
error: error.message ?? 'Execution result error, no data or error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionOutput = {
|
||||||
|
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPendingEvent = actionOutput.pendingEvent;
|
||||||
|
|
||||||
|
const isSuccess = isDefined(actionOutput.result);
|
||||||
|
|
||||||
|
const isError = isDefined(actionOutput.error);
|
||||||
|
|
||||||
|
const isStopped = actionOutput.shouldEndWorkflowRun;
|
||||||
|
|
||||||
|
if (!isError) {
|
||||||
|
this.sendWorkflowNodeRunEvent(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stepInfo: WorkflowRunStepInfo;
|
||||||
|
|
||||||
|
if (isPendingEvent) {
|
||||||
|
stepInfo = {
|
||||||
|
status: StepStatus.PENDING,
|
||||||
|
};
|
||||||
|
} else if (isStopped) {
|
||||||
|
stepInfo = {
|
||||||
|
status: StepStatus.STOPPED,
|
||||||
|
result: actionOutput?.result,
|
||||||
|
};
|
||||||
|
} else if (isSuccess) {
|
||||||
|
stepInfo = {
|
||||||
|
status: StepStatus.SUCCESS,
|
||||||
|
result: actionOutput?.result,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
stepInfo = {
|
||||||
|
status: StepStatus.FAILED,
|
||||||
|
error: actionOutput?.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
|
||||||
|
stepId,
|
||||||
|
stepInfo,
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
isSuccess &&
|
||||||
|
!isStopped &&
|
||||||
|
isDefined(stepToExecute.nextStepIds) &&
|
||||||
|
stepToExecute.nextStepIds.length > 0
|
||||||
|
) {
|
||||||
|
await this.executeFromSteps({
|
||||||
|
stepIds: stepToExecute.nextStepIds,
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
shouldComputeWorkflowRunStatus: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async computeWorkflowRunStatus({
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
workflowRunId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
const workflowRun =
|
||||||
|
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepInfos = workflowRun.state.stepInfos;
|
||||||
|
|
||||||
|
const steps = workflowRun.state.flow.steps;
|
||||||
|
|
||||||
|
if (workflowShouldKeepRunning({ stepInfos, steps })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowShouldFail({ stepInfos, steps })) {
|
||||||
|
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
status: WorkflowRunStatus.FAILED,
|
||||||
|
error: 'WorkflowRun failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||||
|
workflowRunId,
|
||||||
|
workspaceId,
|
||||||
|
status: WorkflowRunStatus.COMPLETED,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendWorkflowNodeRunEvent(workspaceId: string) {
|
private sendWorkflowNodeRunEvent(workspaceId: string) {
|
||||||
@ -265,45 +235,13 @@ export class WorkflowExecutorWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkCanBillWorkflowNodeExecutionOrEndWorkflowRun({
|
private async canBillWorkflowNodeExecution(workspaceId: string) {
|
||||||
stepIdToExecute,
|
return (
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
}: {
|
|
||||||
stepIdToExecute: string;
|
|
||||||
workflowRunId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}) {
|
|
||||||
const canBillWorkflowNodeExecution =
|
|
||||||
!this.billingService.isBillingEnabled() ||
|
!this.billingService.isBillingEnabled() ||
|
||||||
(await this.billingService.canBillMeteredProduct(
|
(await this.billingService.canBillMeteredProduct(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
BillingProductKey.WORKFLOW_NODE_EXECUTION,
|
BillingProductKey.WORKFLOW_NODE_EXECUTION,
|
||||||
));
|
))
|
||||||
|
);
|
||||||
if (!canBillWorkflowNodeExecution) {
|
|
||||||
const billingOutput = {
|
|
||||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
|
||||||
workspaceId,
|
|
||||||
workflowRunId,
|
|
||||||
stepOutput: {
|
|
||||||
id: stepIdToExecute,
|
|
||||||
output: billingOutput,
|
|
||||||
},
|
|
||||||
stepStatus: StepStatus.FAILED,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
status: WorkflowRunStatus.FAILED,
|
|
||||||
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return canBillWorkflowNodeExecution;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { StepStatus } from 'twenty-shared/workflow';
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { WorkflowRunStepInfo } from 'twenty-shared/src/workflow/types/WorkflowRunStateStepInfos';
|
||||||
|
|
||||||
import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
|
import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
|
||||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
@ -13,7 +14,6 @@ import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/compos
|
|||||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import {
|
import {
|
||||||
StepOutput,
|
|
||||||
WorkflowRunState,
|
WorkflowRunState,
|
||||||
WorkflowRunStatus,
|
WorkflowRunStatus,
|
||||||
WorkflowRunWorkspaceEntity,
|
WorkflowRunWorkspaceEntity,
|
||||||
@ -205,16 +205,16 @@ export class WorkflowRunWorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@WithLock('workflowRunId')
|
@WithLock('workflowRunId')
|
||||||
async updateWorkflowRunStepStatus({
|
async updateWorkflowRunStepInfo({
|
||||||
|
stepId,
|
||||||
|
stepInfo,
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
stepId,
|
|
||||||
stepStatus,
|
|
||||||
}: {
|
}: {
|
||||||
workflowRunId: string;
|
|
||||||
stepId: string;
|
stepId: string;
|
||||||
|
stepInfo: WorkflowRunStepInfo;
|
||||||
|
workflowRunId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
stepStatus: StepStatus;
|
|
||||||
}) {
|
}) {
|
||||||
const workflowRunToUpdate = await this.getWorkflowRunOrFail({
|
const workflowRunToUpdate = await this.getWorkflowRunOrFail({
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
@ -227,43 +227,10 @@ export class WorkflowRunWorkspaceService {
|
|||||||
stepInfos: {
|
stepInfos: {
|
||||||
...workflowRunToUpdate.state?.stepInfos,
|
...workflowRunToUpdate.state?.stepInfos,
|
||||||
[stepId]: {
|
[stepId]: {
|
||||||
...(workflowRunToUpdate.state?.stepInfos?.[stepId] || {}),
|
...(workflowRunToUpdate.state?.stepInfos[stepId] || {}),
|
||||||
status: stepStatus,
|
result: stepInfo?.result,
|
||||||
},
|
error: stepInfo?.error,
|
||||||
},
|
status: stepInfo.status,
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.updateWorkflowRun({ workflowRunId, workspaceId, partialUpdate });
|
|
||||||
}
|
|
||||||
|
|
||||||
@WithLock('workflowRunId')
|
|
||||||
async saveWorkflowRunState({
|
|
||||||
workflowRunId,
|
|
||||||
stepOutput,
|
|
||||||
workspaceId,
|
|
||||||
stepStatus,
|
|
||||||
}: {
|
|
||||||
workflowRunId: string;
|
|
||||||
stepOutput: StepOutput;
|
|
||||||
workspaceId: string;
|
|
||||||
stepStatus: StepStatus;
|
|
||||||
}) {
|
|
||||||
const workflowRunToUpdate = await this.getWorkflowRunOrFail({
|
|
||||||
workflowRunId,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const partialUpdate = {
|
|
||||||
state: {
|
|
||||||
...workflowRunToUpdate.state,
|
|
||||||
stepInfos: {
|
|
||||||
...workflowRunToUpdate.state?.stepInfos,
|
|
||||||
[stepOutput.id]: {
|
|
||||||
...(workflowRunToUpdate.state?.stepInfos[stepOutput.id] || {}),
|
|
||||||
result: stepOutput.output?.result,
|
|
||||||
error: stepOutput.output?.error,
|
|
||||||
status: stepStatus,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export enum StepStatus {
|
|||||||
NOT_STARTED = 'NOT_STARTED',
|
NOT_STARTED = 'NOT_STARTED',
|
||||||
RUNNING = 'RUNNING',
|
RUNNING = 'RUNNING',
|
||||||
SUCCESS = 'SUCCESS',
|
SUCCESS = 'SUCCESS',
|
||||||
|
STOPPED = 'STOPPED',
|
||||||
FAILED = 'FAILED',
|
FAILED = 'FAILED',
|
||||||
PENDING = 'PENDING',
|
PENDING = 'PENDING',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user