@@ -41,7 +41,9 @@ const JOB_CHUNK_SIZE = 100
4141const MAX_TICK_DURATION_MS = 3 * 60 * 1000
4242const STALE_SCHEDULE_CLAIM_MS = getMaxExecutionTimeout ( )
4343const STALE_SCHEDULE_RECOVERY_BATCH_SIZE = 100
44- let databaseScheduleStartQueue : Promise < void > = Promise . resolve ( )
44+ const DATABASE_SCHEDULE_START_TURN_WAIT_MS = 1_000
45+ type DatabaseScheduleStartResult = 'started' | 'capacity_full' | 'not_pending'
46+ let databaseScheduleStartTurn : Promise < void > | null = null
4547
4648const dueFilter = ( queuedAt : Date ) =>
4749 and (
@@ -68,18 +70,30 @@ const workflowScheduleFilter = (queuedAt: Date) =>
6870const jobScheduleFilter = ( queuedAt : Date ) =>
6971 and ( dueFilter ( queuedAt ) , sql `${ workflowSchedule . sourceType } = 'job'` )
7072
71- async function runWithDatabaseScheduleStartTurn < T > ( operation : ( ) => Promise < T > ) : Promise < T > {
72- const previousTurn = databaseScheduleStartQueue
73+ async function runWithDatabaseScheduleStartTurn (
74+ operation : ( ) => Promise < DatabaseScheduleStartResult >
75+ ) : Promise < DatabaseScheduleStartResult > {
76+ const activeTurn = databaseScheduleStartTurn
77+ if ( activeTurn ) {
78+ const turnOpened = await Promise . race ( [
79+ activeTurn . then ( ( ) => true ) ,
80+ sleep ( DATABASE_SCHEDULE_START_TURN_WAIT_MS ) . then ( ( ) => false ) ,
81+ ] )
82+ if ( ! turnOpened || databaseScheduleStartTurn ) return 'capacity_full'
83+ }
84+
7385 let releaseTurn = ( ) => { }
74- databaseScheduleStartQueue = new Promise < void > ( ( resolve ) => {
86+ const currentTurn = new Promise < void > ( ( resolve ) => {
7587 releaseTurn = resolve
7688 } )
77-
78- await previousTurn
89+ databaseScheduleStartTurn = currentTurn
7990
8091 try {
8192 return await operation ( )
8293 } finally {
94+ if ( databaseScheduleStartTurn === currentTurn ) {
95+ databaseScheduleStartTurn = null
96+ }
8397 releaseTurn ( )
8498 }
8599}
@@ -286,14 +300,10 @@ function staleScheduleExecutionJobsFilter(staleStartedBefore: Date) {
286300 )
287301}
288302
289- function getExhaustedRecoveryNextRunAt ( payload : ScheduleExecutionPayload , now : Date ) : Date {
290- return (
291- getNextRunFromCronExpression ( payload . cronExpression , payload . timezone ) ??
292- new Date ( now . getTime ( ) + 24 * 60 * 60 * 1000 )
293- )
294- }
295-
296- function getScheduleFailureNextRunAt ( schedule : DatabaseScheduleExecutionTarget , now : Date ) : Date {
303+ function getScheduleNextRunAt (
304+ schedule : { cronExpression ?: string | null ; timezone ?: string } ,
305+ now : Date
306+ ) : Date {
297307 return (
298308 getNextRunFromCronExpression ( schedule . cronExpression , schedule . timezone ) ??
299309 new Date ( now . getTime ( ) + 24 * 60 * 60 * 1000 )
@@ -313,7 +323,7 @@ async function markClaimedScheduleFailed(
313323 updatedAt : now ,
314324 lastQueuedAt : null ,
315325 lastFailedAt : now ,
316- nextRunAt : getScheduleFailureNextRunAt ( schedule , now ) ,
326+ nextRunAt : getScheduleNextRunAt ( schedule , now ) ,
317327 failedCount : sql `COALESCE(${ workflowSchedule . failedCount } , 0) + 1` ,
318328 status : sql `CASE WHEN COALESCE(${ workflowSchedule . failedCount } , 0) + 1 >= ${ MAX_CONSECUTIVE_FAILURES } THEN 'disabled' ELSE 'active' END` ,
319329 infraRetryCount : 0 ,
@@ -447,7 +457,7 @@ async function recoverStaleDatabaseScheduleJobs(now: Date): Promise<void> {
447457 updatedAt : now ,
448458 lastQueuedAt : null ,
449459 lastFailedAt : now ,
450- nextRunAt : getExhaustedRecoveryNextRunAt ( payload , now ) ,
460+ nextRunAt : getScheduleNextRunAt ( payload , now ) ,
451461 failedCount : sql `COALESCE(${ workflowSchedule . failedCount } , 0) + 1` ,
452462 status : sql `CASE WHEN COALESCE(${ workflowSchedule . failedCount } , 0) + 1 >= ${ MAX_CONSECUTIVE_FAILURES } THEN 'disabled' ELSE 'active' END` ,
453463 infraRetryCount : 0 ,
@@ -488,8 +498,6 @@ function isStaleDatabaseScheduleJob(job: { status: string; startedAt?: Date }):
488498}
489499
490500async function getDatabaseScheduleExecutionSlots ( ) : Promise < number > {
491- await recoverStaleDatabaseScheduleJobs ( new Date ( ) )
492-
493501 const [ row ] = await db
494502 . select ( {
495503 count : sql < number > `count(*)` ,
@@ -501,8 +509,6 @@ async function getDatabaseScheduleExecutionSlots(): Promise<number> {
501509 return Math . max ( 0 , SCHEDULE_EXECUTION_CONCURRENCY_LIMIT - processingCount )
502510}
503511
504- type DatabaseScheduleStartResult = 'started' | 'capacity_full' | 'not_pending'
505-
506512async function tryStartDatabaseScheduleJob ( jobId : string ) : Promise < DatabaseScheduleStartResult > {
507513 const now = new Date ( )
508514
@@ -1062,6 +1068,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
10621068 let databaseScheduleSlots = SCHEDULE_EXECUTION_CONCURRENCY_LIMIT
10631069
10641070 if ( useDatabaseFallback ) {
1071+ await recoverStaleDatabaseScheduleJobs ( queuedAt )
10651072 databaseScheduleSlots = await getDatabaseScheduleExecutionSlots ( )
10661073 resumedPendingSchedules = await resumePendingDatabaseScheduleJobs (
10671074 jobQueue ,
0 commit comments