Skip to content

Commit e36ddf6

Browse files
committed
fix(copilot): reconcile failed cancellation rollbacks
1 parent 2a37dbe commit e36ddf6

2 files changed

Lines changed: 74 additions & 1 deletion

File tree

apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,64 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
10941094
expect(mockMarkExecutionCancelled).not.toHaveBeenCalled()
10951095
})
10961096

1097+
it('rolls back pause staging for an already-cancelled execution when cancellation is aborted', async () => {
1098+
const controller = new AbortController()
1099+
dbChainMockFns.limit.mockResolvedValueOnce([
1100+
{
1101+
executionDeadlineAt: null,
1102+
executionOrigin: null,
1103+
status: 'cancelled',
1104+
workspaceId: 'workspace-1',
1105+
},
1106+
])
1107+
mockStagePausedCancellation.mockImplementationOnce(async () => {
1108+
controller.abort()
1109+
return { kind: 'idle' }
1110+
})
1111+
1112+
const response = await cancelWorkflowExecutionPostAuth({
1113+
workflowId: 'wf-1',
1114+
executionId: 'ex-1',
1115+
userId: 'user-1',
1116+
abortSignal: controller.signal,
1117+
})
1118+
1119+
expect(response.status).toBe(409)
1120+
expect(mockClearPausedCancellationIntent).toHaveBeenCalledWith('ex-1', 'wf-1')
1121+
expect(mockWriteTerminalEvent).not.toHaveBeenCalled()
1122+
expect(mockCompletePausedCancellation).not.toHaveBeenCalled()
1123+
expect(mockReleaseExecutionSlot).not.toHaveBeenCalled()
1124+
})
1125+
1126+
it('finishes cancellation when an aborted active-resume stage cannot be rolled back', async () => {
1127+
const controller = new AbortController()
1128+
mockStagePausedCancellation.mockImplementationOnce(async () => {
1129+
controller.abort()
1130+
return { kind: 'active_resume', target: ACTIVE_RESUME_TARGET }
1131+
})
1132+
mockRollbackActiveResumeCancellation.mockResolvedValueOnce(false)
1133+
mockMarkExecutionCancelled.mockResolvedValue({ durablyRecorded: true, reason: 'recorded' })
1134+
mockCompletePausedCancellation.mockResolvedValue(true)
1135+
1136+
const response = await cancelWorkflowExecutionPostAuth({
1137+
workflowId: 'wf-1',
1138+
executionId: 'ex-1',
1139+
userId: 'user-1',
1140+
abortSignal: controller.signal,
1141+
})
1142+
1143+
expect(response.status).toBe(200)
1144+
await expect(response.json()).resolves.toMatchObject({ success: true, pausedCancelled: true })
1145+
expect(mockRollbackActiveResumeCancellation).toHaveBeenCalledWith(
1146+
'ex-1',
1147+
'wf-1',
1148+
'resume-entry-1'
1149+
)
1150+
expect(mockMarkExecutionCancelled).toHaveBeenCalledWith('resume-ex-1', {
1151+
executionDeadlineAt: null,
1152+
})
1153+
})
1154+
10971155
it('returns 404 when the execution does not belong to the workflow', async () => {
10981156
dbChainMockFns.limit.mockResolvedValueOnce([])
10991157

apps/sim/lib/execution/cancel-workflow-execution-post-auth.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,18 @@ async function rollbackPausedCancellationAfterAbort(args: {
356356
if (!abortResponse) return null
357357

358358
if (args.stage.kind === 'active_resume') {
359-
await PauseResumeManager.rollbackActiveResumeCancellation(
359+
const rolledBack = await PauseResumeManager.rollbackActiveResumeCancellation(
360360
args.executionId,
361361
args.workflowId,
362362
args.stage.target.resumeEntryId
363363
)
364+
if (!rolledBack) {
365+
logger.warn('Aborted cancellation could not be rolled back; completing cancellation', {
366+
executionId: args.executionId,
367+
activeResumeEntryId: args.stage.target.resumeEntryId,
368+
})
369+
return null
370+
}
364371
} else if (args.stage.kind === 'idle') {
365372
await PauseResumeManager.clearPausedCancellationIntent(args.executionId, args.workflowId)
366373
}
@@ -516,6 +523,14 @@ export async function cancelWorkflowExecutionPostAuth({
516523
executionId,
517524
workflowId
518525
)
526+
const postStageAbort = await rollbackPausedCancellationAfterAbort({
527+
stage: pausedCancellationStage,
528+
workflowId,
529+
executionId,
530+
abortSignal,
531+
})
532+
if (postStageAbort) return postStageAbort
533+
519534
const hasPausedCancellation = isPausedCancellationStage(pausedCancellationStage)
520535
const requiresCancellationEvent = hasPausedCancellation || isWorkflowGroupExecution
521536
let cancellationEventPublished = !requiresCancellationEvent

0 commit comments

Comments
 (0)