Skip to content

Commit 0892cdb

Browse files
fix(api): preserve cancelled queue status
1 parent 6b8806c commit 0892cdb

9 files changed

Lines changed: 75 additions & 10 deletions

File tree

apps/docs/openapi-core.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1484,7 +1484,7 @@
14841484
},
14851485
"status": {
14861486
"type": "string",
1487-
"enum": ["queued", "processing", "completed", "failed"],
1487+
"enum": ["queued", "processing", "completed", "failed", "cancelled"],
14881488
"description": "Current status of the job.",
14891489
"example": "completed"
14901490
},

apps/sim/app/api/cron/cleanup-stale-executions/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
203203
})
204204
}
205205

206-
// Delete completed/failed jobs older than retention period
207206
const retentionThreshold = new Date(Date.now() - JOB_RETENTION_HOURS * 60 * 60 * 1000)
208207
let asyncJobsDeleted = 0
209208

@@ -212,7 +211,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
212211
.delete(asyncJobs)
213212
.where(
214213
and(
215-
inArray(asyncJobs.status, [JOB_STATUS.COMPLETED, JOB_STATUS.FAILED]),
214+
inArray(asyncJobs.status, [
215+
JOB_STATUS.COMPLETED,
216+
JOB_STATUS.FAILED,
217+
JOB_STATUS.CANCELLED,
218+
]),
216219
lt(asyncJobs.completedAt, retentionThreshold)
217220
)
218221
)

apps/sim/lib/api/contracts/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const getStatusContract = defineRouteContract({
106106
},
107107
})
108108

109-
const jobStatusSchema = z.enum(['pending', 'processing', 'completed', 'failed'])
109+
const jobStatusSchema = z.enum(['pending', 'processing', 'completed', 'failed', 'cancelled'])
110110

111111
const jobStatusResponseSchema = z
112112
.object({

apps/sim/lib/core/async-jobs/backends/database.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,20 @@ describe('DatabaseJobQueue batchEnqueueAndWait', () => {
112112
expect(maxInFlight).toBe(2)
113113
})
114114
})
115+
116+
describe('DatabaseJobQueue cancelJob', () => {
117+
beforeEach(() => {
118+
vi.clearAllMocks()
119+
resetDbChainMock()
120+
})
121+
122+
it('persists cancellation as its own terminal status', async () => {
123+
const queue = new DatabaseJobQueue()
124+
125+
await queue.cancelJob('workflow:1')
126+
127+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
128+
expect.objectContaining({ status: 'cancelled', error: 'Cancelled' })
129+
)
130+
})
131+
})

apps/sim/lib/core/async-jobs/backends/database.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export class DatabaseJobQueue implements JobQueueBackend {
314314
await db
315315
.update(asyncJobs)
316316
.set({
317-
status: JOB_STATUS.FAILED,
317+
status: JOB_STATUS.CANCELLED,
318318
completedAt: now,
319319
error: 'Cancelled',
320320
updatedAt: now,

apps/sim/lib/core/async-jobs/backends/trigger-dev.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,21 @@ describe('TriggerDevJobQueue getJob', () => {
158158
metadata: { workflowId: 'workflow-1' },
159159
})
160160
})
161+
162+
it('preserves a cancelled Trigger.dev run as cancelled', async () => {
163+
mockRetrieveRun.mockResolvedValueOnce({
164+
id: 'run-cancelled',
165+
taskIdentifier: 'workflow-execution',
166+
payload: { workflowId: 'workflow-1' },
167+
status: 'CANCELED',
168+
createdAt: '2026-08-05T12:00:00.000Z',
169+
finishedAt: '2026-08-05T12:00:01.000Z',
170+
attemptCount: 0,
171+
})
172+
const queue = new TriggerDevJobQueue()
173+
174+
const job = await queue.getJob('run-cancelled')
175+
176+
expect(job).toMatchObject({ id: 'run-cancelled', status: 'cancelled' })
177+
})
161178
})

apps/sim/lib/core/async-jobs/backends/trigger-dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function mapTriggerDevStatus(status: string): JobStatus {
6363
case 'COMPLETED':
6464
return JOB_STATUS.COMPLETED
6565
case 'CANCELED':
66+
return JOB_STATUS.CANCELLED
6667
case 'FAILED':
6768
case 'CRASHED':
6869
case 'INTERRUPTED':

apps/sim/lib/core/async-jobs/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
* Types and constants for the async job queue system
33
*/
44

5-
/** Retention period for completed/failed jobs (in hours) */
5+
/** Retention period for terminal jobs (in hours) */
66
export const JOB_RETENTION_HOURS = 24
77

8-
/** Retention period for completed/failed jobs (in seconds, for Redis TTL) */
8+
/** Retention period for terminal jobs (in seconds, for Redis TTL) */
99
export const JOB_RETENTION_SECONDS = JOB_RETENTION_HOURS * 60 * 60
1010

1111
/** Max lifetime for jobs in Redis (in seconds) - cleanup for stuck pending/processing jobs */
@@ -16,6 +16,7 @@ export const JOB_STATUS = {
1616
PROCESSING: 'processing',
1717
COMPLETED: 'completed',
1818
FAILED: 'failed',
19+
CANCELLED: 'cancelled',
1920
} as const
2021

2122
export type JobStatus = (typeof JOB_STATUS)[keyof typeof JOB_STATUS]

apps/sim/lib/workflows/executor/execution-status.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@ vi.mock('@/lib/core/async-jobs', () => ({
1212
getJobQueue: vi.fn().mockResolvedValue({ getJob: mockGetJob }),
1313
}))
1414

15-
vi.mock('@/lib/workflows/executor/enqueue-execution', () => ({
16-
RESUME_EXECUTION_JOB_ID_PREFIX: 'resume-execution:',
17-
WORKFLOW_EXECUTION_JOB_ID_PREFIX: 'workflow-execution:',
15+
vi.mock('@/lib/logs/execution/functional-outputs', () => ({
16+
collectFunctionalBlockOutputs: vi.fn().mockReturnValue(new Map()),
17+
}))
18+
19+
vi.mock('@/lib/logs/execution/trace-store', () => ({
20+
materializeExecutionData: vi.fn(),
21+
}))
22+
23+
vi.mock('@/lib/workflows/executor/paused-execution-metadata', () => ({
24+
getAutomaticResumeWaitingMetadata: vi.fn().mockReturnValue(null),
1825
}))
1926

2027
import { getWorkflowExecutionStatus } from '@/lib/workflows/executor/execution-status'
@@ -56,6 +63,25 @@ describe('getWorkflowExecutionStatus queue projection', () => {
5663
expect(mockGetJob).toHaveBeenCalledWith('workflow-execution:execution-1')
5764
})
5865

66+
it('preserves queue cancellation as a cancelled execution resource', async () => {
67+
mockGetJob.mockResolvedValue({
68+
status: 'cancelled',
69+
createdAt: new Date('2026-08-05T12:00:00.000Z'),
70+
completedAt: new Date('2026-08-05T12:00:01.000Z'),
71+
metadata: { workflowId: 'workflow-1' },
72+
})
73+
74+
const status = await getWorkflowExecutionStatus(input)
75+
76+
expect(status).toMatchObject({
77+
executionId: 'execution-1',
78+
status: 'cancelled',
79+
level: 'info',
80+
endedAt: '2026-08-05T12:00:01.000Z',
81+
error: null,
82+
})
83+
})
84+
5985
it('uses the resume entry ID when the queued work is a resume attempt', async () => {
6086
queueTableRows(schemaMock.resumeQueue, [{ id: 'resume-entry-1', status: 'claimed' }])
6187
mockGetJob.mockResolvedValueOnce({

0 commit comments

Comments
 (0)