mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
feat(core): Declare what on main awaits a queued run's outcome (no-changelog) (#40567)
This commit is contained in:
@@ -955,6 +955,7 @@ describe('ActiveWorkflowManager', () => {
|
||||
mode,
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
|
||||
expect(eventService.emit).toHaveBeenCalledWith('workflow-executed', {
|
||||
@@ -998,6 +999,7 @@ describe('ActiveWorkflowManager', () => {
|
||||
mode,
|
||||
undefined,
|
||||
'wf-1:node-1:1700000000000',
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ describe('WaitTracker', () => {
|
||||
projectId: project.id,
|
||||
pushRef: execution.data.pushRef,
|
||||
userId: undefined,
|
||||
callerAwaitsOutcome: 'none',
|
||||
},
|
||||
false,
|
||||
false,
|
||||
@@ -375,6 +376,7 @@ describe('WaitTracker', () => {
|
||||
workflowData: execution.workflowData,
|
||||
projectId: project.id,
|
||||
pushRef: execution.data.pushRef,
|
||||
callerAwaitsOutcome: 'none',
|
||||
},
|
||||
false,
|
||||
false,
|
||||
@@ -395,6 +397,7 @@ describe('WaitTracker', () => {
|
||||
workflowData: parentExecution.workflowData,
|
||||
projectId: project.id,
|
||||
pushRef: parentExecution.data.pushRef,
|
||||
callerAwaitsOutcome: 'none',
|
||||
startedAt: parentExecution.startedAt,
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -1258,6 +1258,7 @@ describe('enqueueExecution', () => {
|
||||
executionData: undefined,
|
||||
pushRef: 'push-ref',
|
||||
streamingEnabled: true,
|
||||
callerAwaitsOutcome: 'response',
|
||||
});
|
||||
const error = new Error('stop for test purposes');
|
||||
|
||||
@@ -1277,6 +1278,7 @@ describe('enqueueExecution', () => {
|
||||
workflowId: 'workflow-xyz',
|
||||
executionId: '1',
|
||||
restartExecutionId,
|
||||
callerAwaitsOutcome: 'response',
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
@@ -396,6 +396,19 @@ describe('ExecutionService', () => {
|
||||
const erroredRun = (error: boolean) =>
|
||||
[{ error: error ? new Error('boom') : undefined }] as unknown as ITaskData[];
|
||||
|
||||
it('should mark the retry as awaited to completion', async () => {
|
||||
const { service, workflowRunner } = buildRetryService();
|
||||
executionPersistence.findWithUnflattenedData.mockResolvedValue(
|
||||
buildCrashedExecution({ lastNodeExecuted: 'Some Node', runData: {} }),
|
||||
);
|
||||
|
||||
await service.retry(retryArgs());
|
||||
|
||||
expect(workflowRunner.run).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ callerAwaitsOutcome: 'completion' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw when retrying a crashed execution whose runData is undefined', async () => {
|
||||
const { service, workflowRunner } = buildRetryService();
|
||||
executionPersistence.findWithUnflattenedData.mockResolvedValue(
|
||||
|
||||
@@ -280,6 +280,8 @@ export class ExecutionService {
|
||||
retryOf: executionId,
|
||||
workflowData: execution.workflowData,
|
||||
userId: user.id,
|
||||
// The retry caller awaits the result below, so a worker must not park it.
|
||||
callerAwaitsOutcome: 'completion',
|
||||
};
|
||||
|
||||
const { lastNodeExecuted } = data.executionData!.resultData;
|
||||
|
||||
@@ -5289,6 +5289,28 @@ describe('createExecutionAdapter run()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('marks a trigger-mode run as awaited so a worker never parks it at shutdown', async () => {
|
||||
const { adapter, mockWorkflowRunner } = createRunAdapterForTests({
|
||||
id: 'wf-1',
|
||||
nodes: [
|
||||
{
|
||||
id: 'n1',
|
||||
name: 'Schedule',
|
||||
type: 'n8n-nodes-base.scheduleTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
await adapter.run('wf-1');
|
||||
|
||||
const runData = mockWorkflowRunner.run.mock.calls[0][0];
|
||||
expect(runData.executionMode).toBe('trigger');
|
||||
expect(runData.callerAwaitsOutcome).toBe('completion');
|
||||
});
|
||||
|
||||
it('still applies overrides when the workflow has no settings', async () => {
|
||||
const { adapter, mockWorkflowRunner } = createRunAdapterForTests({
|
||||
id: 'wf-1',
|
||||
|
||||
@@ -2153,6 +2153,9 @@ export class InstanceAiAdapterService {
|
||||
},
|
||||
userId: user.id,
|
||||
pushRef,
|
||||
// The agent awaits this run's result and prunes its pins afterwards, so
|
||||
// a worker must not park a trigger-mode run at shutdown.
|
||||
callerAwaitsOutcome: 'completion',
|
||||
// A verification run picks a production execution mode above so the
|
||||
// trigger behaves realistically. Without this opt-out, a failed build
|
||||
// attempt would dispatch the user's error workflow as if production
|
||||
|
||||
@@ -22,6 +22,8 @@ export type JobData = {
|
||||
loadStaticData: boolean;
|
||||
pushRef?: string;
|
||||
streamingEnabled?: boolean;
|
||||
/** What on main awaits this run's outcome, see IWorkflowExecutionDataProcess. */
|
||||
callerAwaitsOutcome?: 'none' | 'response' | 'completion';
|
||||
restartExecutionId?: string;
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
|
||||
@@ -174,6 +174,9 @@ export class WaitTracker {
|
||||
projectId: project.id,
|
||||
pushRef: fullExecutionData.data.pushRef,
|
||||
startedAt: fullExecutionData.startedAt,
|
||||
// Whatever awaited this run on main was answered when the run paused, so
|
||||
// a resumed segment owes nothing and may be suspended again.
|
||||
callerAwaitsOutcome: 'none',
|
||||
// Not a stored field, so a resume has to derive it again — otherwise the
|
||||
// run comes back without an identity and a credential only its publisher
|
||||
// may use is refused halfway through.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { EventService } from '@n8n/backend-services';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { EngineConfig } from '@n8n/config';
|
||||
import { EngineConfig, ExecutionsConfig } from '@n8n/config';
|
||||
import type { Project, User } from '@n8n/db';
|
||||
import { UserRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
INodeType,
|
||||
IDataObject,
|
||||
IWebhookResponseData,
|
||||
WebhookResponseMode,
|
||||
IN8nHttpFullResponse,
|
||||
IWorkflowBase,
|
||||
IRunExecutionData,
|
||||
@@ -1632,6 +1633,8 @@ describe('executeWebhook credential-status gate', () => {
|
||||
authentication: string;
|
||||
gateResult?: CredentialCheckResult;
|
||||
webhookResult?: IWebhookResponseData;
|
||||
responseMode?: WebhookResponseMode;
|
||||
startNodeType?: string;
|
||||
}) => {
|
||||
const checkCredentialStatus = vi.fn().mockResolvedValue(options.gateResult);
|
||||
|
||||
@@ -1649,7 +1652,7 @@ describe('executeWebhook credential-status gate', () => {
|
||||
|
||||
const workflowStartNode = mock<INode>({
|
||||
name: 'Webhook',
|
||||
type: WEBHOOK_NODE_TYPE,
|
||||
type: options.startNodeType ?? WEBHOOK_NODE_TYPE,
|
||||
typeVersion: 2,
|
||||
parameters: { authentication: options.authentication },
|
||||
});
|
||||
@@ -1669,7 +1672,7 @@ describe('executeWebhook credential-status gate', () => {
|
||||
.mockReturnValue(mock<INodeType>({ description: { name: 'webhook' } })),
|
||||
},
|
||||
expression: {
|
||||
getSimpleParameterValue: vi.fn().mockReturnValue('onReceived'),
|
||||
getSimpleParameterValue: vi.fn().mockReturnValue(options.responseMode ?? 'onReceived'),
|
||||
getComplexParameterValue: vi.fn().mockReturnValue('firstEntryJson'),
|
||||
},
|
||||
});
|
||||
@@ -1747,6 +1750,54 @@ describe('executeWebhook credential-status gate', () => {
|
||||
expect(workflowRunner.run).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['onReceived', 'hostedChat'] as const)(
|
||||
'marks a %s run as owing no webhook response, since main answers at enqueue',
|
||||
async (responseMode) => {
|
||||
// Not awaited: non-onReceived modes then wait on a post-execute promise that never settles here.
|
||||
void runGate({ authentication: 'none', gateResult: missingGateResult, responseMode });
|
||||
await vi.waitFor(() => expect(workflowRunner.run).toHaveBeenCalled());
|
||||
|
||||
const [runData] = vi.mocked(workflowRunner.run).mock.calls[0];
|
||||
expect(runData.callerAwaitsOutcome).toBe('none');
|
||||
},
|
||||
);
|
||||
|
||||
describe('in queue mode', () => {
|
||||
const executionsConfig = Container.get(ExecutionsConfig);
|
||||
|
||||
beforeEach(() => {
|
||||
executionsConfig.mode = 'queue';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
executionsConfig.mode = 'regular';
|
||||
});
|
||||
|
||||
it('marks an MCP Trigger run as awaited to completion, since the worker relays the MCP response', async () => {
|
||||
void runGate({
|
||||
authentication: 'none',
|
||||
gateResult: missingGateResult,
|
||||
startNodeType: MCP_TRIGGER_NODE_TYPE,
|
||||
});
|
||||
await vi.waitFor(() => expect(workflowRunner.run).toHaveBeenCalled());
|
||||
|
||||
const [runData] = vi.mocked(workflowRunner.run).mock.calls[0];
|
||||
expect(runData.callerAwaitsOutcome).toBe('completion');
|
||||
});
|
||||
});
|
||||
|
||||
it('marks a lastNode run as owing a webhook response', async () => {
|
||||
void runGate({
|
||||
authentication: 'none',
|
||||
gateResult: missingGateResult,
|
||||
responseMode: 'lastNode',
|
||||
});
|
||||
await vi.waitFor(() => expect(workflowRunner.run).toHaveBeenCalled());
|
||||
|
||||
const [runData] = vi.mocked(workflowRunner.run).mock.calls[0];
|
||||
expect(runData.callerAwaitsOutcome).toBe('response');
|
||||
});
|
||||
|
||||
it('does not gate webhooks that do not establish a triggering identity', async () => {
|
||||
const { checkCredentialStatus, responseCallback } = await runGate({
|
||||
authentication: 'none',
|
||||
|
||||
@@ -121,6 +121,13 @@ const SUPPORTED_RESPONSE_MODES = new Set<WebhookResponseMode>([
|
||||
'hostedChat',
|
||||
]);
|
||||
|
||||
/** Modes where the caller is answered as soon as the run is enqueued, so the run itself owes no response. */
|
||||
const IMMEDIATE_RESPONSE_MODES = new Set<WebhookResponseMode>([
|
||||
'onReceived',
|
||||
'formPage',
|
||||
'hostedChat',
|
||||
]);
|
||||
|
||||
interface WebhookInvocationResult {
|
||||
webhookResultData: IWebhookResponseData;
|
||||
runExecutionDataChanges: WebhookExecutionDataChanges;
|
||||
@@ -222,6 +229,8 @@ async function prepareMcpQueueExecution(
|
||||
|
||||
runData.isMcpExecution = true;
|
||||
runData.mcpType = 'trigger';
|
||||
// The worker relays the MCP response when the run ends, so a paused segment would be taken for the result.
|
||||
runData.callerAwaitsOutcome = 'completion';
|
||||
runData.mcpSessionId = mcpSessionId;
|
||||
runData.mcpMessageId = mcpMessageId;
|
||||
|
||||
@@ -1187,6 +1196,7 @@ export async function executeWebhook(
|
||||
projectName: project?.name,
|
||||
userId: webhookData.userId,
|
||||
encryptedRunnerIdentity: additionalData.encryptedRunnerIdentity,
|
||||
callerAwaitsOutcome: IMMEDIATE_RESPONSE_MODES.has(responseMode) ? 'none' : 'response',
|
||||
// v1 reads this from `executionData.startData`, which `prepareExecutionData`
|
||||
// sets, so carrying it here changes nothing for v1. Engine v2 has no way to
|
||||
// stop at a node, and its dispatcher refuses the run on this field.
|
||||
|
||||
@@ -764,6 +764,7 @@ export class WorkflowRunner {
|
||||
loadStaticData: !!loadStaticData,
|
||||
pushRef: data.pushRef,
|
||||
streamingEnabled: data.streamingEnabled,
|
||||
callerAwaitsOutcome: data.callerAwaitsOutcome,
|
||||
restartExecutionId,
|
||||
projectId: data.projectId,
|
||||
projectName: data.projectName,
|
||||
|
||||
@@ -320,6 +320,50 @@ describe('WorkflowExecutionService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('marks a fire-and-forget polled run as suspendable', async () => {
|
||||
await workflowExecutionService.runPolledWorkflow(
|
||||
workflow,
|
||||
node,
|
||||
pollItems,
|
||||
additionalData,
|
||||
'trigger',
|
||||
cursor,
|
||||
);
|
||||
|
||||
expect(workflowRunner.run).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ callerAwaitsOutcome: 'none' }),
|
||||
false,
|
||||
undefined,
|
||||
{ executionId: 'exec-9', expectedStatus: 'new' },
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test.each(['response', 'completion'] as const)(
|
||||
'forwards the %s caller state for a polled run',
|
||||
async (callerAwaitsOutcome) => {
|
||||
await workflowExecutionService.runPolledWorkflow(
|
||||
workflow,
|
||||
node,
|
||||
pollItems,
|
||||
additionalData,
|
||||
'trigger',
|
||||
cursor,
|
||||
responsePromise,
|
||||
undefined,
|
||||
callerAwaitsOutcome,
|
||||
);
|
||||
|
||||
expect(workflowRunner.run).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ callerAwaitsOutcome }),
|
||||
false,
|
||||
undefined,
|
||||
{ executionId: 'exec-9', expectedStatus: 'new' },
|
||||
responsePromise,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('prepares the new execution before commit, then starts the run without reloading static data', async () => {
|
||||
const callOrder: string[] = [];
|
||||
workflowRunner.prepareNewExecution.mockImplementation(async () => {
|
||||
|
||||
+91
@@ -159,6 +159,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
mode,
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
expect(eventService.emit).toHaveBeenCalledWith('workflow-executed', {
|
||||
workflowId: workflowData.id,
|
||||
@@ -222,6 +223,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
'trigger',
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -264,6 +266,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
'trigger',
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -297,6 +300,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
mode,
|
||||
undefined,
|
||||
'wf-1:node-1:1700000000000',
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -327,6 +331,52 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
|
||||
expect(activeExecutions.getPostExecutePromise).toHaveBeenCalledWith('exec-123');
|
||||
await expect(donePromise.promise).resolves.toBe(runResult);
|
||||
// The node waits for the run's end, so the run must not be parked at worker shutdown.
|
||||
expect(workflowExecutionService.runWorkflow).toHaveBeenCalledWith(
|
||||
workflowData,
|
||||
node,
|
||||
[[]],
|
||||
expect.anything(),
|
||||
mode,
|
||||
undefined,
|
||||
undefined,
|
||||
'completion',
|
||||
);
|
||||
});
|
||||
|
||||
test('marks a run as awaited for its response when the trigger passes only a response promise', async () => {
|
||||
const workflowData = mock<WorkflowEntity>({ id: 'wf-1', name: 'Test Workflow' });
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const mode: WorkflowExecuteMode = 'trigger';
|
||||
const activation: WorkflowActivateMode = 'activate';
|
||||
const workflow = mock<Workflow>({ name: 'Test Workflow' });
|
||||
const node = mock<INode>({ name: 'Trigger Node' });
|
||||
|
||||
const getTriggerFunctions = factory.getExecuteTriggerFunctions(
|
||||
workflowData,
|
||||
additionalData,
|
||||
mode,
|
||||
activation,
|
||||
async () => workflowData,
|
||||
vi.fn(),
|
||||
scheduleCollectionSession,
|
||||
);
|
||||
const context = getTriggerFunctions(workflow, node, additionalData, mode, activation);
|
||||
const responsePromise = createDeferredPromise<IExecuteResponsePromiseData>();
|
||||
|
||||
context.emit([[]], responsePromise);
|
||||
await sleep(0);
|
||||
|
||||
expect(workflowExecutionService.runWorkflow).toHaveBeenCalledWith(
|
||||
workflowData,
|
||||
node,
|
||||
[[]],
|
||||
expect.anything(),
|
||||
mode,
|
||||
responsePromise,
|
||||
undefined,
|
||||
'response',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not emit workflow-executed on DuplicateExecutionError', async () => {
|
||||
@@ -720,6 +770,8 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
expect.objectContaining({ userId: undefined }),
|
||||
mode,
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -769,6 +821,8 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
expect.objectContaining({ userId: 'publisher-of-live-version' }),
|
||||
'trigger',
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -808,6 +862,8 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
expect.objectContaining({ userId: undefined }),
|
||||
'trigger',
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1132,10 +1188,43 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
{ lastItemId: 'a' },
|
||||
responsePromise,
|
||||
undefined,
|
||||
'response',
|
||||
);
|
||||
expect(workflowExecutionService.runWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ hasResponse: false, hasDone: false, callerAwaitsOutcome: 'none' },
|
||||
{ hasResponse: false, hasDone: true, callerAwaitsOutcome: 'completion' },
|
||||
{ hasResponse: true, hasDone: true, callerAwaitsOutcome: 'completion' },
|
||||
])(
|
||||
'passes $callerAwaitsOutcome for a cursor-backed poll with response=$hasResponse and done=$hasDone',
|
||||
async ({ hasResponse, hasDone, callerAwaitsOutcome }) => {
|
||||
const responsePromise = hasResponse
|
||||
? createDeferredPromise<IExecuteResponsePromiseData>()
|
||||
: undefined;
|
||||
const donePromise = hasDone ? createDeferredPromise<IRun>() : undefined;
|
||||
|
||||
await context.__runPoll(async () => {
|
||||
context.getWorkflowStaticData('node').lastItemId = 'a';
|
||||
context.__emit(pollData, responsePromise, donePromise);
|
||||
});
|
||||
await sleep(0);
|
||||
|
||||
expect(workflowExecutionService.runPolledWorkflow).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
node,
|
||||
pollData,
|
||||
expect.anything(),
|
||||
mode,
|
||||
{ lastItemId: 'a' },
|
||||
responsePromise,
|
||||
undefined,
|
||||
callerAwaitsOutcome,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('routes to runPolledWorkflowV2, not the transactional runPolledWorkflow, on engine v2', async () => {
|
||||
engineV2Dispatcher.handlesWorkflow.mockReturnValue(true);
|
||||
workflowExecutionService.runPolledWorkflowV2.mockResolvedValue('exec-v2');
|
||||
@@ -1371,6 +1460,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
{ lastItemId: 'first-only' },
|
||||
undefined,
|
||||
undefined,
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1602,6 +1692,7 @@ describe('TriggerExecutionContextFactory', () => {
|
||||
{ lastItemId: 'a' },
|
||||
undefined,
|
||||
fence,
|
||||
'none',
|
||||
);
|
||||
|
||||
await fencedContext.__runPoll(async () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
IRun,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
IWorkflowExecutionDataProcess,
|
||||
PollCursor,
|
||||
WorkflowActivateMode,
|
||||
WorkflowExecuteMode,
|
||||
@@ -50,6 +51,16 @@ import { WorkflowPublishedDataService } from '@/workflows/workflow-published-dat
|
||||
import { WorkflowPublisherService } from '@/workflows/workflow-publisher.service';
|
||||
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
|
||||
|
||||
/** What the trigger node waits on from the run it starts. A done promise outranks a response. */
|
||||
function awaitedByTrigger(
|
||||
responsePromise: IDeferredPromise<IExecuteResponsePromiseData> | undefined,
|
||||
donePromise: IDeferredPromise<IRun | undefined> | undefined,
|
||||
): NonNullable<IWorkflowExecutionDataProcess['callerAwaitsOutcome']> {
|
||||
if (donePromise) return 'completion';
|
||||
if (responsePromise) return 'response';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export type TriggerFailureHandler = (opts: {
|
||||
error: Error;
|
||||
node: INode;
|
||||
@@ -218,6 +229,8 @@ export class TriggerExecutionContextFactory {
|
||||
mode,
|
||||
responsePromise,
|
||||
deduplicationKey,
|
||||
// A node awaiting the run's end, or a response from it, must not get a paused segment instead.
|
||||
awaitedByTrigger(responsePromise, donePromise),
|
||||
);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -390,6 +403,8 @@ export class TriggerExecutionContextFactory {
|
||||
runAdditionalData,
|
||||
mode,
|
||||
responsePromise,
|
||||
undefined,
|
||||
awaitedByTrigger(responsePromise, donePromise),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -413,6 +428,7 @@ export class TriggerExecutionContextFactory {
|
||||
cursor,
|
||||
responsePromise,
|
||||
fence,
|
||||
awaitedByTrigger(responsePromise, donePromise),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ export class WorkflowExecutionService {
|
||||
mode: WorkflowExecuteMode,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
deduplicationKey?: string,
|
||||
callerAwaitsOutcome: IWorkflowExecutionDataProcess['callerAwaitsOutcome'] = 'none',
|
||||
) {
|
||||
const nodeExecutionStack: IExecuteData[] = [
|
||||
{
|
||||
@@ -120,6 +121,7 @@ export class WorkflowExecutionService {
|
||||
deduplicationKey,
|
||||
projectId,
|
||||
projectName,
|
||||
callerAwaitsOutcome,
|
||||
};
|
||||
|
||||
return await this.workflowRunner.run(runData, true, undefined, undefined, responsePromise);
|
||||
@@ -138,6 +140,7 @@ export class WorkflowExecutionService {
|
||||
cursor: PollCursor,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
fence?: PollLeaseFence,
|
||||
callerAwaitsOutcome: IWorkflowExecutionDataProcess['callerAwaitsOutcome'] = 'none',
|
||||
): Promise<string | undefined> {
|
||||
const nodeExecutionStack: IExecuteData[] = [
|
||||
{
|
||||
@@ -167,6 +170,7 @@ export class WorkflowExecutionService {
|
||||
workflowData,
|
||||
projectId,
|
||||
projectName,
|
||||
callerAwaitsOutcome,
|
||||
};
|
||||
|
||||
// Mask the trigger items before the payload is committed, so the persisted row
|
||||
|
||||
@@ -3801,6 +3801,18 @@ export interface IWorkflowExecutionDataProcess {
|
||||
agentRequest?: AiAgentRequest;
|
||||
httpResponse?: express.Response; // Used for streaming responses
|
||||
streamingEnabled?: boolean;
|
||||
/**
|
||||
* What, if anything, on the main that started this run awaits its outcome
|
||||
* and would act on a paused segment as if it were the result. A worker must
|
||||
* not park such a run at shutdown.
|
||||
* - `'response'`: a webhook caller is owed a response; satisfied once the run
|
||||
* relays one.
|
||||
* - `'completion'`: a trigger node's done promise (Kafka, RabbitMQ, MQTT), an
|
||||
* Instance AI run or a retry awaits the end of the run; never satisfied early.
|
||||
* - `'none'`: nothing waits, the run may be parked.
|
||||
* Absent (older main): treated as `'completion'`, the strictest reading.
|
||||
*/
|
||||
callerAwaitsOutcome?: 'none' | 'response' | 'completion';
|
||||
/**
|
||||
* Only engine v2 reads this. The data-plane execution id, set by a caller that
|
||||
* minted it before the run starts: to subscribe to the run's answer, or because
|
||||
|
||||
Reference in New Issue
Block a user