Skip to content

Commit 0085e68

Browse files
akshay326cursoragent
andcommitted
test(execution): cover inline admission lease
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent cd57158 commit 0085e68

3 files changed

Lines changed: 50 additions & 17 deletions

File tree

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ const {
3434
mockReleaseExecutionIdClaim,
3535
mockReleaseExecutionSlot,
3636
mockRequireBillingAttributionHeader,
37+
mockShouldExecuteInline,
38+
mockStartJob,
39+
mockCompleteJob,
40+
mockMarkJobFailed,
41+
mockExecuteWorkflowJob,
3742
mockValidatePublicApiAllowed,
3843
} = vi.hoisted(() => ({
3944
mockAssertBillingAttributionSnapshot: vi.fn((value: unknown) => {
@@ -52,6 +57,11 @@ const {
5257
mockReleaseExecutionIdClaim: vi.fn(),
5358
mockReleaseExecutionSlot: vi.fn(),
5459
mockRequireBillingAttributionHeader: vi.fn(),
60+
mockShouldExecuteInline: vi.fn(() => false),
61+
mockStartJob: vi.fn(),
62+
mockCompleteJob: vi.fn(),
63+
mockMarkJobFailed: vi.fn(),
64+
mockExecuteWorkflowJob: vi.fn(),
5565
mockValidatePublicApiAllowed: vi.fn(),
5666
}))
5767

@@ -114,11 +124,11 @@ vi.mock('@/lib/execution/payloads/store', () => ({
114124
vi.mock('@/lib/core/async-jobs', () => ({
115125
getJobQueue: vi.fn().mockResolvedValue({
116126
enqueue: mockEnqueue,
117-
startJob: vi.fn(),
118-
completeJob: vi.fn(),
119-
markJobFailed: vi.fn(),
127+
startJob: mockStartJob,
128+
completeJob: mockCompleteJob,
129+
markJobFailed: mockMarkJobFailed,
120130
}),
121-
shouldExecuteInline: vi.fn().mockReturnValue(false),
131+
shouldExecuteInline: mockShouldExecuteInline,
122132
}))
123133

124134
vi.mock('@/lib/core/utils/urls', () => ({
@@ -136,7 +146,7 @@ vi.mock('@/lib/execution/call-chain', () => ({
136146
vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)
137147

138148
vi.mock('@/background/workflow-execution', () => ({
139-
executeWorkflowJob: vi.fn(),
149+
executeWorkflowJob: mockExecuteWorkflowJob,
140150
}))
141151

142152
vi.mock('@sim/utils/id', () => ({
@@ -288,6 +298,8 @@ describe('workflow execute async route', () => {
288298
beforeEach(() => {
289299
vi.clearAllMocks()
290300
resetDbChainMock()
301+
mockShouldExecuteInline.mockReturnValue(false)
302+
mockExecuteWorkflowJob.mockResolvedValue({ success: true })
291303
mockGenerateId.mockReset().mockReturnValue('execution-123')
292304
mockClaimExecutionId.mockImplementation(async (executionId: string) => ({
293305
key: `workflow-execution-id:${executionId}`,
@@ -392,6 +404,36 @@ describe('workflow execute async route', () => {
392404
)
393405
})
394406

407+
it('retains the admission ticket until database-backed async execution finishes', async () => {
408+
mockShouldExecuteInline.mockReturnValue(true)
409+
let resolveInlineExecution!: () => void
410+
const inlineExecution = new Promise<void>((resolve) => {
411+
resolveInlineExecution = resolve
412+
})
413+
mockExecuteWorkflowJob.mockReturnValueOnce(inlineExecution)
414+
415+
const response = await POST(
416+
createMockRequest(
417+
'POST',
418+
{ input: { hello: 'world' } },
419+
{
420+
'Content-Type': 'application/json',
421+
'X-Execution-Mode': 'async',
422+
}
423+
),
424+
{ params: Promise.resolve({ id: 'workflow-1' }) }
425+
)
426+
427+
expect(response.status).toBe(202)
428+
expect(getAdmissionGateStatus().inflight).toBe(1)
429+
430+
resolveInlineExecution()
431+
await vi.waitFor(() => {
432+
expect(getAdmissionGateStatus().inflight).toBe(0)
433+
})
434+
expect(mockCompleteJob).toHaveBeenCalledWith('job-123', expect.anything())
435+
})
436+
395437
it('applies admission backpressure to session-backed async executions', async () => {
396438
hybridAuthMockFns.mockHasExternalApiCredentials.mockReturnValue(false)
397439
const heldTickets = Array.from({ length: getAdmissionGateStatus().maxInflight }, () =>

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,6 @@ type AsyncExecutionParams = {
285285
interface AsyncExecutionResult {
286286
response: NextResponse
287287
retainExecutionClaim: boolean
288-
/**
289-
* When true, the caller must not release the admission ticket — ownership
290-
* transferred to the in-process inline runner which releases on completion.
291-
*/
292-
retainAdmissionTicket: boolean
293288
}
294289

295290
/** Mutable bag so async inline work can retain the HTTP admission ticket. */
@@ -391,7 +386,6 @@ async function handleAsyncExecution(
391386
return {
392387
response: NextResponse.json({ error: 'Failed to queue async execution' }, { status: 500 }),
393388
retainExecutionClaim: false,
394-
retainAdmissionTicket: false,
395389
}
396390
}
397391

@@ -443,7 +437,6 @@ async function handleAsyncExecution(
443437
return {
444438
response: NextResponse.json({ error: 'Failed to queue async execution' }, { status: 500 }),
445439
retainExecutionClaim: false,
446-
retainAdmissionTicket: false,
447440
}
448441
}
449442

@@ -457,7 +450,6 @@ async function handleAsyncExecution(
457450
{ status: 503, headers: { [WORKFLOW_EXECUTION_ID_HEADER]: executionId } }
458451
),
459452
retainExecutionClaim: true,
460-
retainAdmissionTicket: false,
461453
}
462454
}
463455

@@ -521,7 +513,6 @@ async function handleAsyncExecution(
521513
{ status: 202 }
522514
),
523515
retainExecutionClaim: true,
524-
retainAdmissionTicket: executeInline,
525516
}
526517
}
527518

apps/sim/scripts/load/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ These local-only Artillery scenarios exercise `POST /api/workflows/[id]/execute`
1010

1111
The default rates are tuned for these local limits:
1212

13-
- `ADMISSION_GATE_MAX_INFLIGHT=500`
13+
- `ADMISSION_GATE_MAX_INFLIGHT=10`
1414

1515
The admission gate caps total in-flight requests per pod.
1616

@@ -23,7 +23,7 @@ Default profile:
2323
- Starts at `2` requests per second
2424
- Ramps to `8` requests per second
2525
- Holds there for `20` seconds
26-
- Good for validating queueing against a Free workspace concurrency of `5`
26+
- Good for validating queueing against a Free workspace concurrency of `10`
2727

2828
```bash
2929
WORKFLOW_ID=<workflow-id> \
@@ -103,5 +103,5 @@ Optional variables:
103103
- `load:workflow` is an alias for `load:workflow:baseline`
104104
- All scenarios send `x-execution-mode: async`
105105
- Artillery output will show request counts and response codes, which is usually enough for quick local verification
106-
- At these defaults, you should see `429` responses when approaching `ADMISSION_GATE_MAX_INFLIGHT=500`
106+
- At these defaults, you should see `429` responses when approaching `ADMISSION_GATE_MAX_INFLIGHT=10`
107107
- If you still see lots of `429` or `ETIMEDOUT` responses locally, lower the rates again before increasing durations

0 commit comments

Comments
 (0)