feat(core): Add workflow cancellation events to log streaming (#23151)

This commit is contained in:
Marc Littlemore
2025-12-15 13:06:22 +00:00
committed by GitHub
parent 173666fd71
commit 568dba2c8f
8 changed files with 72 additions and 7 deletions
+8 -1
View File
@@ -178,7 +178,14 @@ export class ActiveExecutions {
// There is no execution running with that id
return;
}
this.eventService.emit('execution-cancelled', { executionId });
const workflowData = execution.executionData.workflowData;
this.eventService.emit('execution-cancelled', {
executionId,
workflowId: workflowData?.id,
workflowName: workflowData?.name,
reason: cancellationError.reason,
});
execution.responsePromise?.reject(cancellationError);
if (execution.status === 'waiting') {
// A waiting execution will not have a valid workflowExecution or postExecutePromise
@@ -47,6 +47,7 @@ export const eventNamesWorkflow = [
'n8n.workflow.started',
'n8n.workflow.success',
'n8n.workflow.failed',
'n8n.workflow.cancelled',
] as const;
export const eventNamesGeneric = ['n8n.worker.started', 'n8n.worker.stopped'] as const;
export const eventNamesNode = ['n8n.node.started', 'n8n.node.finished'] as const;
@@ -1083,6 +1083,30 @@ describe('LogStreamingEventRelay', () => {
},
});
});
it.each(['manual', 'timeout', 'shutdown'] as const)(
'should log on `execution-cancelled` event with %s reason',
(reason) => {
const event: RelayEventMap['execution-cancelled'] = {
executionId: 'exec-cancelled-123',
workflowId: 'wf-456',
workflowName: 'Cancelled Workflow',
reason,
};
eventService.emit('execution-cancelled', event);
expect(eventBus.sendWorkflowEvent).toHaveBeenCalledWith({
eventName: 'n8n.workflow.cancelled',
payload: {
executionId: 'exec-cancelled-123',
workflowId: 'wf-456',
workflowName: 'Cancelled Workflow',
reason,
},
});
},
);
});
describe('AI events', () => {
@@ -1,6 +1,7 @@
import type { AuthenticationMethod, ProjectRelation } from '@n8n/api-types';
import type { AuthProviderType, User, IWorkflowDb } from '@n8n/db';
import type {
CancellationReason,
IPersonalizationSurveyAnswersV4,
IRun,
IWorkflowBase,
@@ -395,6 +396,9 @@ export type RelayEventMap = {
'execution-cancelled': {
executionId: string;
workflowId?: string;
workflowName?: string;
reason: CancellationReason;
};
// #endregion
@@ -51,6 +51,7 @@ export class LogStreamingEventRelay extends EventRelay {
'community-package-deleted': (event) => this.communityPackageDeleted(event),
'execution-throttled': (event) => this.executionThrottled(event),
'execution-started-during-bootup': (event) => this.executionStartedDuringBootup(event),
'execution-cancelled': (event) => this.executionCancelled(event),
'ai-messages-retrieved-from-memory': (event) => this.aiMessagesRetrievedFromMemory(event),
'ai-message-added-to-memory': (event) => this.aiMessageAddedToMemory(event),
'ai-output-parsed': (event) => this.aiOutputParsed(event),
@@ -465,6 +466,23 @@ export class LogStreamingEventRelay extends EventRelay {
});
}
private executionCancelled({
executionId,
workflowId,
workflowName,
reason,
}: RelayEventMap['execution-cancelled']) {
void this.eventBus.sendWorkflowEvent({
eventName: 'n8n.workflow.cancelled',
payload: {
executionId,
workflowId,
workflowName,
reason,
},
});
}
// #endregion
// #region AI
+7 -2
View File
@@ -287,8 +287,13 @@ export class JobProcessor {
const runningJob = this.runningJobs[jobId];
if (!runningJob) return;
const executionId = runningJob.executionId;
this.eventService.emit('execution-cancelled', { executionId });
const { executionId, workflowId, workflowName } = runningJob;
this.eventService.emit('execution-cancelled', {
executionId,
workflowId,
workflowName,
reason: 'manual', // Job stops via scaling service are always user-initiated
});
runningJob.run.cancel();
delete this.runningJobs[jobId];
@@ -1,32 +1,37 @@
import { ExecutionBaseError } from './abstract/execution-base.error';
export type CancellationReason = 'manual' | 'timeout' | 'shutdown';
export abstract class ExecutionCancelledError extends ExecutionBaseError {
readonly reason: CancellationReason;
// NOTE: prefer one of the more specific
constructor(executionId: string) {
constructor(executionId: string, reason: CancellationReason) {
super('The execution was cancelled', {
level: 'warning',
extra: { executionId },
});
this.reason = reason;
}
}
export class ManualExecutionCancelledError extends ExecutionCancelledError {
constructor(executionId: string) {
super(executionId);
super(executionId, 'manual');
this.message = 'The execution was cancelled manually';
}
}
export class TimeoutExecutionCancelledError extends ExecutionCancelledError {
constructor(executionId: string) {
super(executionId);
super(executionId, 'timeout');
this.message = 'The execution was cancelled because it timed out';
}
}
export class SystemShutdownExecutionCancelledError extends ExecutionCancelledError {
constructor(executionId: string) {
super(executionId);
super(executionId, 'shutdown');
this.message = 'The execution was cancelled because the system is shutting down';
}
}
+1
View File
@@ -9,6 +9,7 @@ export {
ManualExecutionCancelledError,
SystemShutdownExecutionCancelledError,
TimeoutExecutionCancelledError,
type CancellationReason,
} from './execution-cancelled.error';
export { NodeApiError } from './node-api.error';
export { NodeOperationError } from './node-operation.error';