@@ -4,6 +4,7 @@ import { publishSyncEvent } from "@/server/queue/sync-events";
44import { syncUserContributions } from "@/server/sync/syncService" ;
55
66const MAX_BATCH_SIZE = 20 ;
7+ const STALE_RUNNING_MS = 10 * 60 * 1000 ;
78const SyncJobStatus = {
89 QUEUED : "QUEUED" ,
910 RUNNING : "RUNNING" ,
@@ -18,6 +19,26 @@ function toBackoffMs(attemptCount: number): number {
1819export async function processSupabaseSyncQueue ( limit = 5 ) : Promise < { picked : number ; completed : number ; failed : number } > {
1920 const batchSize = Math . max ( 1 , Math . min ( MAX_BATCH_SIZE , limit ) ) ;
2021 const now = new Date ( ) ;
22+
23+ // Recover jobs left RUNNING when a serverless execution is interrupted (timeout/redeploy/crash).
24+ const staleBefore = new Date ( Date . now ( ) - STALE_RUNNING_MS ) ;
25+ const recovered = await ( prisma as any ) . syncJob . updateMany ( {
26+ where : {
27+ status : SyncJobStatus . RUNNING ,
28+ OR : [ { lockedAt : { lte : staleBefore } } , { lockedAt : null , startedAt : { lte : staleBefore } } ] ,
29+ } ,
30+ data : {
31+ status : SyncJobStatus . QUEUED ,
32+ availableAt : now ,
33+ lockedAt : null ,
34+ startedAt : null ,
35+ errorMessage : "Recovered stale RUNNING lock after processor interruption." ,
36+ } ,
37+ } ) ;
38+ if ( recovered . count > 0 ) {
39+ safeLog ( "warn" , "Recovered stale supabase sync jobs" , { count : recovered . count } ) ;
40+ }
41+
2142 const candidates = await ( prisma as any ) . syncJob . findMany ( {
2243 where : {
2344 status : SyncJobStatus . QUEUED ,
0 commit comments