@@ -2,7 +2,7 @@ import { db, workflowDeploymentVersion, workflowSchedule } from '@sim/db'
22import { createLogger } from '@sim/logger'
33import { toError } from '@sim/utils/errors'
44import { generateId } from '@sim/utils/id'
5- import { and , eq , isNull , lt , lte , ne , not , or , sql } from 'drizzle-orm'
5+ import { and , eq , inArray , isNull , lt , lte , ne , not , or , sql } from 'drizzle-orm'
66import { type NextRequest , NextResponse } from 'next/server'
77import { verifyCronAuth } from '@/lib/auth/internal'
88import { getJobQueue , shouldExecuteInline } from '@/lib/core/async-jobs'
@@ -17,6 +17,9 @@ import {
1717export const dynamic = 'force-dynamic'
1818
1919const logger = createLogger ( 'ScheduledExecuteAPI' )
20+ const MAX_CRON_CLAIMS = 20
21+ const RESERVED_WORKFLOW_CLAIMS = 10
22+ const RESERVED_JOB_CLAIMS = MAX_CRON_CLAIMS - RESERVED_WORKFLOW_CLAIMS
2023
2124const dueFilter = ( queuedAt : Date ) =>
2225 and (
@@ -30,27 +33,42 @@ const dueFilter = (queuedAt: Date) =>
3033 )
3134 )
3235
33- export const GET = withRouteHandler ( async ( request : NextRequest ) => {
34- const requestId = generateRequestId ( )
35- logger . info ( `[${ requestId } ] Scheduled execution triggered at ${ new Date ( ) . toISOString ( ) } ` )
36+ const activeWorkflowDeploymentFilter = ( ) =>
37+ sql `${ workflowSchedule . deploymentVersionId } = (select ${ workflowDeploymentVersion . id } from ${ workflowDeploymentVersion } where ${ workflowDeploymentVersion . workflowId } = ${ workflowSchedule . workflowId } and ${ workflowDeploymentVersion . isActive } = true)`
3638
37- const authError = verifyCronAuth ( request , 'Schedule execution' )
38- if ( authError ) {
39- return authError
40- }
39+ const workflowScheduleFilter = ( queuedAt : Date ) =>
40+ and (
41+ dueFilter ( queuedAt ) ,
42+ or ( eq ( workflowSchedule . sourceType , 'workflow' ) , isNull ( workflowSchedule . sourceType ) ) ,
43+ activeWorkflowDeploymentFilter ( )
44+ )
4145
42- const queuedAt = new Date ( )
46+ const jobScheduleFilter = ( queuedAt : Date ) =>
47+ and ( dueFilter ( queuedAt ) , eq ( workflowSchedule . sourceType , 'job' ) )
4348
44- try {
45- // Workflow schedules (require active deployment)
46- const dueSchedules = await db
49+ async function claimWorkflowSchedules ( queuedAt : Date , limit : number ) {
50+ if ( limit <= 0 ) return [ ]
51+
52+ return db . transaction ( async ( tx ) => {
53+ const rows = await tx
54+ . select ( { id : workflowSchedule . id } )
55+ . from ( workflowSchedule )
56+ . where ( workflowScheduleFilter ( queuedAt ) )
57+ . for ( 'update' , { skipLocked : true } )
58+ . limit ( limit )
59+
60+ if ( rows . length === 0 ) return [ ]
61+
62+ return tx
4763 . update ( workflowSchedule )
4864 . set ( { lastQueuedAt : queuedAt , updatedAt : queuedAt } )
4965 . where (
5066 and (
51- dueFilter ( queuedAt ) ,
52- or ( eq ( workflowSchedule . sourceType , 'workflow' ) , isNull ( workflowSchedule . sourceType ) ) ,
53- sql `${ workflowSchedule . deploymentVersionId } = (select ${ workflowDeploymentVersion . id } from ${ workflowDeploymentVersion } where ${ workflowDeploymentVersion . workflowId } = ${ workflowSchedule . workflowId } and ${ workflowDeploymentVersion . isActive } = true)`
67+ workflowScheduleFilter ( queuedAt ) ,
68+ inArray (
69+ workflowSchedule . id ,
70+ rows . map ( ( row ) => row . id )
71+ )
5472 )
5573 )
5674 . returning ( {
@@ -62,21 +80,68 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
6280 failedCount : workflowSchedule . failedCount ,
6381 nextRunAt : workflowSchedule . nextRunAt ,
6482 lastQueuedAt : workflowSchedule . lastQueuedAt ,
83+ deploymentVersionId : workflowSchedule . deploymentVersionId ,
6584 sourceType : workflowSchedule . sourceType ,
6685 } )
86+ } )
87+ }
6788
68- // Jobs (no deployment, dispatch inline)
69- const dueJobs = await db
89+ async function claimJobSchedules ( queuedAt : Date , limit : number ) {
90+ if ( limit <= 0 ) return [ ]
91+
92+ return db . transaction ( async ( tx ) => {
93+ const rows = await tx
94+ . select ( { id : workflowSchedule . id } )
95+ . from ( workflowSchedule )
96+ . where ( jobScheduleFilter ( queuedAt ) )
97+ . for ( 'update' , { skipLocked : true } )
98+ . limit ( limit )
99+
100+ if ( rows . length === 0 ) return [ ]
101+
102+ return tx
70103 . update ( workflowSchedule )
71104 . set ( { lastQueuedAt : queuedAt , updatedAt : queuedAt } )
72- . where ( and ( dueFilter ( queuedAt ) , eq ( workflowSchedule . sourceType , 'job' ) ) )
105+ . where (
106+ and (
107+ jobScheduleFilter ( queuedAt ) ,
108+ inArray (
109+ workflowSchedule . id ,
110+ rows . map ( ( row ) => row . id )
111+ )
112+ )
113+ )
73114 . returning ( {
74115 id : workflowSchedule . id ,
75116 cronExpression : workflowSchedule . cronExpression ,
76117 failedCount : workflowSchedule . failedCount ,
77118 lastQueuedAt : workflowSchedule . lastQueuedAt ,
78119 sourceType : workflowSchedule . sourceType ,
79120 } )
121+ } )
122+ }
123+
124+ export const GET = withRouteHandler ( async ( request : NextRequest ) => {
125+ const requestId = generateRequestId ( )
126+ logger . info ( `[${ requestId } ] Scheduled execution triggered at ${ new Date ( ) . toISOString ( ) } ` )
127+
128+ const authError = verifyCronAuth ( request , 'Schedule execution' )
129+ if ( authError ) {
130+ return authError
131+ }
132+
133+ const queuedAt = new Date ( )
134+
135+ try {
136+ const dueSchedules = await claimWorkflowSchedules ( queuedAt , RESERVED_WORKFLOW_CLAIMS )
137+ const dueJobs = await claimJobSchedules ( queuedAt , RESERVED_JOB_CLAIMS )
138+ const remainingClaimBudget = Math . max ( 0 , MAX_CRON_CLAIMS - dueSchedules . length - dueJobs . length )
139+
140+ if ( remainingClaimBudget > 0 && dueSchedules . length === RESERVED_WORKFLOW_CLAIMS ) {
141+ dueSchedules . push ( ...( await claimWorkflowSchedules ( queuedAt , remainingClaimBudget ) ) )
142+ } else if ( remainingClaimBudget > 0 && dueJobs . length === RESERVED_JOB_CLAIMS ) {
143+ dueJobs . push ( ...( await claimJobSchedules ( queuedAt , remainingClaimBudget ) ) )
144+ }
80145
81146 const totalCount = dueSchedules . length + dueJobs . length
82147 logger . info (
@@ -108,6 +173,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
108173 requestId,
109174 correlation,
110175 blockId : schedule . blockId || undefined ,
176+ deploymentVersionId : schedule . deploymentVersionId || undefined ,
111177 cronExpression : schedule . cronExpression || undefined ,
112178 lastRanAt : schedule . lastRanAt ?. toISOString ( ) ,
113179 failedCount : schedule . failedCount || 0 ,
0 commit comments