Skip to content

Commit 51b0cf1

Browse files
refactor(execution): extract enqueue/status/cancel into shared libs
Prepares the v2 execution surface: handleAsyncExecution's queue logic moves to lib/workflows/executor/enqueue-execution.ts (slot/claim semantics encoded in a discriminated outcome, not HTTP statuses), the execution-status read to execution-status.ts, and the order-sensitive cancel machinery to lib/execution/cancel-workflow-execution.ts. The v1 routes re-render identically — their suites pass unmodified. Also: preprocessExecution gains rateLimitCounter ('sync'|'async') and its 429 now carries code RATE_LIMIT_EXCEEDED + retryAfterMs (previously indistinguishable from the concurrency 429 and Retry-After was discarded); and the duplicate cancel contract in contracts/logs.ts is unified on the full 5-value reason enum — its narrower copy made requestJson throw a client ZodError when cancelling a paused HITL run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
1 parent f6c9cdb commit 51b0cf1

9 files changed

Lines changed: 774 additions & 612 deletions

File tree

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 9 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import {
2020
requireBillingAttributionHeader,
2121
} from '@/lib/billing/core/billing-attribution'
2222
import { admissionRejectedResponse, tryAdmit } from '@/lib/core/admission/gate'
23-
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
24-
import { isAsyncJobEnqueueError } from '@/lib/core/async-jobs/types'
2523
import {
2624
createTimeoutAbortController,
2725
getTimeoutErrorMessage,
@@ -69,6 +67,7 @@ import {
6967
hydrateUserFilesWithBase64,
7068
} from '@/lib/uploads/utils/user-file-base64.server'
7169
import { getCustomBlockRowsForWorkspace } from '@/lib/workflows/custom-blocks/operations'
70+
import { enqueueWorkflowExecution } from '@/lib/workflows/executor/enqueue-execution'
7271
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
7372
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
7473
import {
@@ -105,7 +104,6 @@ import {
105104
} from '@/lib/workflows/streaming/streaming'
106105
import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
107106
import { getWorkspaceBillingSettings } from '@/lib/workspaces/utils'
108-
import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/workflow-execution'
109107
import { withCustomBlockOverlay } from '@/blocks/custom/server-overlay'
110108
import {
111109
PublicApiNotAllowedError,
@@ -127,8 +125,6 @@ import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/
127125
const logger = createLogger('WorkflowExecuteAPI')
128126
const MAX_WORKFLOW_EXECUTE_BODY_BYTES = 10 * 1024 * 1024
129127
const SERVER_EXECUTION_ID_CLAIM_ATTEMPTS = 3
130-
const ASYNC_ENQUEUE_ATTEMPTS = 2
131-
const WORKFLOW_EXECUTION_JOB_ID_PREFIX = 'workflow-execution:'
132128

133129
export const runtime = 'nodejs'
134130
export const dynamic = 'force-dynamic'
@@ -338,170 +334,38 @@ function requirePreprocessedExecutionContext(
338334
}
339335

340336
async function handleAsyncExecution(params: AsyncExecutionParams): Promise<AsyncExecutionResult> {
341-
const {
342-
requestId,
343-
workflowId,
344-
userId,
345-
billingAttribution,
346-
workspaceId,
347-
input,
348-
triggerType,
349-
executionId,
350-
callChain,
351-
} = params
352-
const asyncLogger = logger.withMetadata({
353-
requestId,
354-
workflowId,
355-
workspaceId,
356-
userId,
357-
executionId,
358-
})
359-
360-
const correlation = {
361-
executionId,
362-
requestId,
363-
source: 'workflow' as const,
364-
workflowId,
365-
triggerType,
366-
}
367-
368-
const payload: WorkflowExecutionPayload = {
369-
workflowId,
370-
userId,
371-
billingAttribution,
372-
workspaceId,
373-
input,
374-
triggerType,
375-
executionId,
376-
requestId,
377-
correlation,
378-
callChain,
379-
executionMode: 'async',
380-
admissionCompleted: true,
381-
}
337+
const enqueue = await enqueueWorkflowExecution(params)
382338

383-
let jobQueue: Awaited<ReturnType<typeof getJobQueue>>
384-
try {
385-
jobQueue = await getJobQueue()
386-
} catch (error) {
387-
asyncLogger.error('Failed to initialize async execution queue', {
388-
error: toError(error).message,
389-
})
390-
await releaseExecutionSlot(executionId)
339+
if (enqueue.outcome === 'rejected') {
391340
return {
392341
response: NextResponse.json({ error: 'Failed to queue async execution' }, { status: 500 }),
393342
retainExecutionClaim: false,
394343
}
395344
}
396345

397-
const deterministicJobId = `${WORKFLOW_EXECUTION_JOB_ID_PREFIX}${executionId}`
398-
const enqueueOptions = {
399-
jobId: deterministicJobId,
400-
metadata: { workflowId, workspaceId, userId, correlation },
401-
}
402-
let jobId: string | undefined
403-
let enqueueError: unknown
404-
let acceptanceCouldBeUnknown = false
405-
406-
for (let attempt = 1; attempt <= ASYNC_ENQUEUE_ATTEMPTS; attempt++) {
407-
try {
408-
jobId = await jobQueue.enqueue('workflow-execution', payload, enqueueOptions)
409-
enqueueError = undefined
410-
break
411-
} catch (error) {
412-
enqueueError = error
413-
const classifiedError = isAsyncJobEnqueueError(error) ? error : undefined
414-
const attemptAcceptance = classifiedError?.acceptance ?? 'unknown'
415-
acceptanceCouldBeUnknown ||= attemptAcceptance === 'unknown'
416-
asyncLogger.warn('Async workflow enqueue attempt failed', {
417-
acceptance: attemptAcceptance,
418-
attempt,
419-
error: toError(error).message,
420-
jobId: deterministicJobId,
421-
})
422-
if (classifiedError?.retryable === false || attempt === ASYNC_ENQUEUE_ATTEMPTS) {
423-
break
424-
}
425-
}
426-
}
427-
428-
if (!jobId) {
429-
const acceptance = acceptanceCouldBeUnknown
430-
? 'unknown'
431-
: isAsyncJobEnqueueError(enqueueError)
432-
? enqueueError.acceptance
433-
: 'unknown'
434-
asyncLogger.error('Failed to queue async execution', {
435-
acceptance,
436-
error: toError(enqueueError).message,
437-
jobId: deterministicJobId,
438-
})
439-
440-
if (acceptance === 'rejected') {
441-
await releaseExecutionSlot(executionId)
442-
return {
443-
response: NextResponse.json({ error: 'Failed to queue async execution' }, { status: 500 }),
444-
retainExecutionClaim: false,
445-
}
446-
}
447-
346+
if (enqueue.outcome === 'ambiguous') {
448347
return {
449348
response: NextResponse.json(
450349
{
451350
error: 'Async execution queue acceptance could not be confirmed',
452351
code: 'ASYNC_ENQUEUE_AMBIGUOUS',
453-
executionId,
352+
executionId: enqueue.executionId,
454353
},
455-
{ status: 503, headers: { [WORKFLOW_EXECUTION_ID_HEADER]: executionId } }
354+
{ status: 503, headers: { [WORKFLOW_EXECUTION_ID_HEADER]: enqueue.executionId } }
456355
),
457356
retainExecutionClaim: true,
458357
}
459358
}
460359

461-
asyncLogger.info('Queued async workflow execution', { jobId })
462-
463-
if (shouldExecuteInline()) {
464-
void (async () => {
465-
let workerOwnsReservation = false
466-
try {
467-
await jobQueue.startJob(jobId)
468-
workerOwnsReservation = true
469-
const output = await executeWorkflowJob(payload)
470-
await jobQueue.completeJob(jobId, output)
471-
} catch (error) {
472-
const errorMessage = toError(error).message
473-
asyncLogger.error('Async workflow execution failed', {
474-
jobId,
475-
error: errorMessage,
476-
})
477-
/**
478-
* Before worker ownership transfers, no LoggingSession exists to
479-
* release the route's reservation.
480-
*/
481-
if (!workerOwnsReservation) {
482-
await releaseExecutionSlot(executionId)
483-
}
484-
try {
485-
await jobQueue.markJobFailed(jobId, errorMessage)
486-
} catch (markFailedError) {
487-
asyncLogger.error('Failed to mark job as failed', {
488-
jobId,
489-
error: toError(markFailedError).message,
490-
})
491-
}
492-
}
493-
})()
494-
}
495-
496360
return {
497361
response: NextResponse.json(
498362
{
499363
success: true,
500364
async: true,
501-
jobId,
502-
executionId,
365+
jobId: enqueue.jobId,
366+
executionId: enqueue.executionId,
503367
message: 'Workflow execution queued',
504-
statusUrl: `${getBaseUrl()}/api/jobs/${jobId}`,
368+
statusUrl: `${getBaseUrl()}/api/jobs/${enqueue.jobId}`,
505369
},
506370
{ status: 202 }
507371
),

0 commit comments

Comments
 (0)