Skip to content

Commit ee06ee3

Browse files
committed
consolidate
1 parent 39d7589 commit ee06ee3

File tree

7 files changed

+146
-124
lines changed

7 files changed

+146
-124
lines changed

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

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
55
import { z } from 'zod'
66
import { checkHybridAuth } from '@/lib/auth/hybrid'
77
import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
8-
import { getTimeoutErrorMessage, isTimeoutError } from '@/lib/core/execution-limits'
8+
import {
9+
createTimeoutAbortController,
10+
getTimeoutErrorMessage,
11+
isTimeoutError,
12+
} from '@/lib/core/execution-limits'
913
import { generateRequestId } from '@/lib/core/utils/request'
1014
import { SSE_HEADERS } from '@/lib/core/utils/sse'
1115
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -402,17 +406,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
402406

403407
if (!enableSSE) {
404408
logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`)
405-
const syncTimeout = preprocessResult.executionTimeout?.sync
406-
const abortController = new AbortController()
407-
let isTimedOut = false
408-
let timeoutId: NodeJS.Timeout | undefined
409-
410-
if (syncTimeout) {
411-
timeoutId = setTimeout(() => {
412-
isTimedOut = true
413-
abortController.abort()
414-
}, syncTimeout)
415-
}
409+
const timeoutController = createTimeoutAbortController(
410+
preprocessResult.executionTimeout?.sync
411+
)
416412

417413
try {
418414
const metadata: ExecutionMetadata = {
@@ -447,12 +443,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
447443
includeFileBase64,
448444
base64MaxBytes,
449445
stopAfterBlockId,
450-
abortSignal: abortController.signal,
446+
abortSignal: timeoutController.signal,
451447
})
452448

453-
if (result.status === 'cancelled' && isTimedOut && syncTimeout) {
454-
const timeoutErrorMessage = getTimeoutErrorMessage(null, syncTimeout)
455-
logger.info(`[${requestId}] Non-SSE execution timed out`, { timeoutMs: syncTimeout })
449+
if (
450+
result.status === 'cancelled' &&
451+
timeoutController.isTimedOut() &&
452+
timeoutController.timeoutMs
453+
) {
454+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
455+
logger.info(`[${requestId}] Non-SSE execution timed out`, {
456+
timeoutMs: timeoutController.timeoutMs,
457+
})
456458
await loggingSession.markAsFailed(timeoutErrorMessage)
457459

458460
await cleanupExecutionBase64Cache(executionId)
@@ -535,7 +537,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
535537
{ status: 500 }
536538
)
537539
} finally {
538-
if (timeoutId) clearTimeout(timeoutId)
540+
timeoutController.cleanup()
539541
}
540542
}
541543

@@ -578,18 +580,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
578580
}
579581

580582
const encoder = new TextEncoder()
581-
const abortController = new AbortController()
583+
const timeoutController = createTimeoutAbortController(preprocessResult.executionTimeout?.sync)
582584
let isStreamClosed = false
583-
let isTimedOut = false
584-
585-
const syncTimeout = preprocessResult.executionTimeout?.sync
586-
let timeoutId: NodeJS.Timeout | undefined
587-
if (syncTimeout) {
588-
timeoutId = setTimeout(() => {
589-
isTimedOut = true
590-
abortController.abort()
591-
}, syncTimeout)
592-
}
593585

594586
const stream = new ReadableStream<Uint8Array>({
595587
async start(controller) {
@@ -780,7 +772,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
780772
onStream,
781773
},
782774
loggingSession,
783-
abortSignal: abortController.signal,
775+
abortSignal: timeoutController.signal,
784776
includeFileBase64,
785777
base64MaxBytes,
786778
stopAfterBlockId,
@@ -816,9 +808,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
816808
}
817809

818810
if (result.status === 'cancelled') {
819-
if (isTimedOut && syncTimeout) {
820-
const timeoutErrorMessage = getTimeoutErrorMessage(null, syncTimeout)
821-
logger.info(`[${requestId}] Workflow execution timed out`, { timeoutMs: syncTimeout })
811+
if (timeoutController.isTimedOut() && timeoutController.timeoutMs) {
812+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
813+
logger.info(`[${requestId}] Workflow execution timed out`, {
814+
timeoutMs: timeoutController.timeoutMs,
815+
})
822816

823817
await loggingSession.markAsFailed(timeoutErrorMessage)
824818

@@ -871,9 +865,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
871865
// Cleanup base64 cache for this execution
872866
await cleanupExecutionBase64Cache(executionId)
873867
} catch (error: unknown) {
874-
const isTimeout = isTimeoutError(error) || isTimedOut
868+
const isTimeout = isTimeoutError(error) || timeoutController.isTimedOut()
875869
const errorMessage = isTimeout
876-
? getTimeoutErrorMessage(error, syncTimeout)
870+
? getTimeoutErrorMessage(error, timeoutController.timeoutMs)
877871
: error instanceof Error
878872
? error.message
879873
: 'Unknown error'
@@ -899,7 +893,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
899893
},
900894
})
901895
} finally {
902-
if (timeoutId) clearTimeout(timeoutId)
896+
timeoutController.cleanup()
903897
if (!isStreamClosed) {
904898
try {
905899
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
@@ -910,9 +904,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
910904
},
911905
cancel() {
912906
isStreamClosed = true
913-
if (timeoutId) clearTimeout(timeoutId)
907+
timeoutController.cleanup()
914908
logger.info(`[${requestId}] Client aborted SSE stream, signalling cancellation`)
915-
abortController.abort()
909+
timeoutController.abort()
916910
markExecutionCancelled(executionId).catch(() => {})
917911
},
918912
})

apps/sim/background/schedule-execution.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { task } from '@trigger.dev/sdk'
44
import { Cron } from 'croner'
55
import { eq } from 'drizzle-orm'
66
import { v4 as uuidv4 } from 'uuid'
7-
import { getTimeoutErrorMessage } from '@/lib/core/execution-limits'
7+
import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits'
88
import { preprocessExecution } from '@/lib/execution/preprocessing'
99
import { LoggingSession } from '@/lib/logs/execution/logging-session'
1010
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
@@ -184,16 +184,7 @@ async function runWorkflowExecution({
184184
[]
185185
)
186186

187-
const abortController = new AbortController()
188-
let isTimedOut = false
189-
let timeoutId: NodeJS.Timeout | undefined
190-
191-
if (asyncTimeout) {
192-
timeoutId = setTimeout(() => {
193-
isTimedOut = true
194-
abortController.abort()
195-
}, asyncTimeout)
196-
}
187+
const timeoutController = createTimeoutAbortController(asyncTimeout)
197188

198189
let executionResult
199190
try {
@@ -203,16 +194,20 @@ async function runWorkflowExecution({
203194
loggingSession,
204195
includeFileBase64: true,
205196
base64MaxBytes: undefined,
206-
abortSignal: abortController.signal,
197+
abortSignal: timeoutController.signal,
207198
})
208199
} finally {
209-
if (timeoutId) clearTimeout(timeoutId)
200+
timeoutController.cleanup()
210201
}
211202

212-
if (executionResult.status === 'cancelled' && isTimedOut && asyncTimeout) {
213-
const timeoutErrorMessage = getTimeoutErrorMessage(null, asyncTimeout)
203+
if (
204+
executionResult.status === 'cancelled' &&
205+
timeoutController.isTimedOut() &&
206+
timeoutController.timeoutMs
207+
) {
208+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
214209
logger.info(`[${requestId}] Scheduled workflow execution timed out`, {
215-
timeoutMs: asyncTimeout,
210+
timeoutMs: timeoutController.timeoutMs,
216211
})
217212
await loggingSession.markAsFailed(timeoutErrorMessage)
218213
} else if (executionResult.status === 'paused') {

apps/sim/background/webhook-execution.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { task } from '@trigger.dev/sdk'
55
import { eq } from 'drizzle-orm'
66
import { v4 as uuidv4 } from 'uuid'
77
import { getHighestPrioritySubscription } from '@/lib/billing'
8-
import { getExecutionTimeout, getTimeoutErrorMessage } from '@/lib/core/execution-limits'
8+
import {
9+
createTimeoutAbortController,
10+
getExecutionTimeout,
11+
getTimeoutErrorMessage,
12+
} from '@/lib/core/execution-limits'
913
import { IdempotencyService, webhookIdempotency } from '@/lib/core/idempotency'
1014
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
1115
import { processExecutionFiles } from '@/lib/execution/files'
@@ -142,16 +146,7 @@ async function executeWebhookJobInternal(
142146
userSubscription?.plan as SubscriptionPlan | undefined,
143147
'async'
144148
)
145-
const abortController = new AbortController()
146-
let isTimedOut = false
147-
let timeoutId: NodeJS.Timeout | undefined
148-
149-
if (asyncTimeout) {
150-
timeoutId = setTimeout(() => {
151-
isTimedOut = true
152-
abortController.abort()
153-
}, asyncTimeout)
154-
}
149+
const timeoutController = createTimeoutAbortController(asyncTimeout)
155150

156151
let deploymentVersionId: string | undefined
157152

@@ -261,13 +256,17 @@ async function executeWebhookJobInternal(
261256
loggingSession,
262257
includeFileBase64: true,
263258
base64MaxBytes: undefined,
264-
abortSignal: abortController.signal,
259+
abortSignal: timeoutController.signal,
265260
})
266261

267-
if (executionResult.status === 'cancelled' && isTimedOut && asyncTimeout) {
268-
const timeoutErrorMessage = getTimeoutErrorMessage(null, asyncTimeout)
262+
if (
263+
executionResult.status === 'cancelled' &&
264+
timeoutController.isTimedOut() &&
265+
timeoutController.timeoutMs
266+
) {
267+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
269268
logger.info(`[${requestId}] Airtable webhook execution timed out`, {
270-
timeoutMs: asyncTimeout,
269+
timeoutMs: timeoutController.timeoutMs,
271270
})
272271
await loggingSession.markAsFailed(timeoutErrorMessage)
273272
} else if (executionResult.status === 'paused') {
@@ -522,12 +521,18 @@ async function executeWebhookJobInternal(
522521
callbacks: {},
523522
loggingSession,
524523
includeFileBase64: true,
525-
abortSignal: abortController.signal,
524+
abortSignal: timeoutController.signal,
526525
})
527526

528-
if (executionResult.status === 'cancelled' && isTimedOut && asyncTimeout) {
529-
const timeoutErrorMessage = getTimeoutErrorMessage(null, asyncTimeout)
530-
logger.info(`[${requestId}] Webhook execution timed out`, { timeoutMs: asyncTimeout })
527+
if (
528+
executionResult.status === 'cancelled' &&
529+
timeoutController.isTimedOut() &&
530+
timeoutController.timeoutMs
531+
) {
532+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
533+
logger.info(`[${requestId}] Webhook execution timed out`, {
534+
timeoutMs: timeoutController.timeoutMs,
535+
})
531536
await loggingSession.markAsFailed(timeoutErrorMessage)
532537
} else if (executionResult.status === 'paused') {
533538
if (!executionResult.snapshotSeed) {
@@ -632,7 +637,7 @@ async function executeWebhookJobInternal(
632637

633638
throw error
634639
} finally {
635-
if (timeoutId) clearTimeout(timeoutId)
640+
timeoutController.cleanup()
636641
}
637642
}
638643

apps/sim/background/workflow-execution.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { task } from '@trigger.dev/sdk'
33
import { v4 as uuidv4 } from 'uuid'
4-
import { getTimeoutErrorMessage } from '@/lib/core/execution-limits'
4+
import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits'
55
import { preprocessExecution } from '@/lib/execution/preprocessing'
66
import { LoggingSession } from '@/lib/logs/execution/logging-session'
77
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
@@ -104,17 +104,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
104104
[]
105105
)
106106

107-
const asyncTimeout = preprocessResult.executionTimeout?.async
108-
const abortController = new AbortController()
109-
let isTimedOut = false
110-
let timeoutId: NodeJS.Timeout | undefined
111-
112-
if (asyncTimeout) {
113-
timeoutId = setTimeout(() => {
114-
isTimedOut = true
115-
abortController.abort()
116-
}, asyncTimeout)
117-
}
107+
const timeoutController = createTimeoutAbortController(preprocessResult.executionTimeout?.async)
118108

119109
let result
120110
try {
@@ -124,15 +114,21 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
124114
loggingSession,
125115
includeFileBase64: true,
126116
base64MaxBytes: undefined,
127-
abortSignal: abortController.signal,
117+
abortSignal: timeoutController.signal,
128118
})
129119
} finally {
130-
if (timeoutId) clearTimeout(timeoutId)
120+
timeoutController.cleanup()
131121
}
132122

133-
if (result.status === 'cancelled' && isTimedOut && asyncTimeout) {
134-
const timeoutErrorMessage = getTimeoutErrorMessage(null, asyncTimeout)
135-
logger.info(`[${requestId}] Workflow execution timed out`, { timeoutMs: asyncTimeout })
123+
if (
124+
result.status === 'cancelled' &&
125+
timeoutController.isTimedOut() &&
126+
timeoutController.timeoutMs
127+
) {
128+
const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs)
129+
logger.info(`[${requestId}] Workflow execution timed out`, {
130+
timeoutMs: timeoutController.timeoutMs,
131+
})
136132
await loggingSession.markAsFailed(timeoutErrorMessage)
137133
} else if (result.status === 'paused') {
138134
if (!result.snapshotSeed) {

apps/sim/lib/core/execution-limits/types.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,43 @@ export function getTimeoutErrorMessage(error: unknown, timeoutMs?: number): stri
9292

9393
return 'Execution timed out'
9494
}
95+
96+
/**
97+
* Helper to create an AbortController with timeout handling.
98+
* Centralizes the timeout abort pattern used across execution paths.
99+
*/
100+
export interface TimeoutAbortController {
101+
/** The AbortSignal to pass to execution functions */
102+
signal: AbortSignal
103+
/** Returns true if the abort was triggered by timeout (not user cancellation) */
104+
isTimedOut: () => boolean
105+
/** Cleanup function - call in finally block to clear the timeout */
106+
cleanup: () => void
107+
/** Manually abort the execution (for user cancellation) */
108+
abort: () => void
109+
/** The timeout duration in milliseconds (undefined if no timeout) */
110+
timeoutMs: number | undefined
111+
}
112+
113+
export function createTimeoutAbortController(timeoutMs?: number): TimeoutAbortController {
114+
const abortController = new AbortController()
115+
let isTimedOut = false
116+
let timeoutId: NodeJS.Timeout | undefined
117+
118+
if (timeoutMs) {
119+
timeoutId = setTimeout(() => {
120+
isTimedOut = true
121+
abortController.abort()
122+
}, timeoutMs)
123+
}
124+
125+
return {
126+
signal: abortController.signal,
127+
isTimedOut: () => isTimedOut,
128+
cleanup: () => {
129+
if (timeoutId) clearTimeout(timeoutId)
130+
},
131+
abort: () => abortController.abort(),
132+
timeoutMs,
133+
}
134+
}

0 commit comments

Comments
 (0)