@@ -33,6 +33,10 @@ export const DESKTOP_RECORDING_LEASE_MS = 5 * 60 * 1_000;
3333export const DESKTOP_RECORDING_SOURCE_RETRY_MS = 60 * 60 * 1_000 ;
3434export const DESKTOP_RECORDING_OUTPUT_REPLACED = "output-replaced" ;
3535export const DESKTOP_RECORDING_DELETING = "video-deleting" ;
36+ export const DESKTOP_RECORDING_RETRY_EXHAUSTED = "processing-retry-exhausted" ;
37+ const MAX_AUTOMATIC_ATTEMPTS = 5 ;
38+ const RETRY_EXHAUSTED_MESSAGE =
39+ "Processing paused after repeated failures. Your uploaded recording is retained. Please contact support." ;
3640
3741const sourceSchema = z . object ( {
3842 version : z . literal ( 1 ) ,
@@ -158,6 +162,7 @@ export function isDesktopRecordingJobRecoverable(
158162 now : Date ,
159163) {
160164 if ( job . state === "verified" ) return false ;
165+ if ( job . errorCode === DESKTOP_RECORDING_RETRY_EXHAUSTED ) return false ;
161166 if ( job . errorCode === DESKTOP_RECORDING_OUTPUT_REPLACED ) return false ;
162167 if ( job . errorCode === DESKTOP_RECORDING_DELETING ) return false ;
163168 if ( job . state === "source-blocked" && job . source ) return false ;
@@ -333,7 +338,11 @@ export async function ensureSegmentProcessingJob({
333338 . where ( eq ( videoProcessingJobs . videoId , videoId ) ) ;
334339 }
335340 }
336- if ( job . state === "source-blocked" && ! job . source ) {
341+ if (
342+ job . state === "source-blocked" &&
343+ ! job . source &&
344+ job . errorCode !== DESKTOP_RECORDING_RETRY_EXHAUSTED
345+ ) {
337346 job = {
338347 ...job ,
339348 state : "committing" ,
@@ -374,6 +383,33 @@ export async function getProcessingState({
374383 return row ? parseDesktopRecordingJob ( row ) : null ;
375384}
376385
386+ async function pauseExhaustedRecording (
387+ tx : Parameters < Parameters < ReturnType < typeof db > [ "transaction" ] > [ 0 ] > [ 0 ] ,
388+ videoId : Video . VideoId ,
389+ errorMessage : string ,
390+ now : Date ,
391+ ) {
392+ await tx
393+ . update ( videoProcessingJobs )
394+ . set ( {
395+ state : "source-blocked" ,
396+ leaseExpiresAt : null ,
397+ errorCode : DESKTOP_RECORDING_RETRY_EXHAUSTED ,
398+ errorMessage,
399+ updatedAt : now ,
400+ } )
401+ . where ( eq ( videoProcessingJobs . videoId , videoId ) ) ;
402+ await tx
403+ . update ( videoUploads )
404+ . set ( {
405+ phase : "error" ,
406+ processingMessage : RETRY_EXHAUSTED_MESSAGE ,
407+ processingError : RETRY_EXHAUSTED_MESSAGE ,
408+ updatedAt : now ,
409+ } )
410+ . where ( eq ( videoUploads . videoId , videoId ) ) ;
411+ }
412+
377413export async function claimProcessingAttempt ( {
378414 videoId,
379415 generation,
@@ -397,6 +433,15 @@ export async function claimProcessingAttempt({
397433 if ( ! row ) return null ;
398434 const job = parseDesktopRecordingJob ( row ) ;
399435 if ( ! isDesktopRecordingJobRecoverable ( job , now ) ) return null ;
436+ if ( job . attemptCount >= MAX_AUTOMATIC_ATTEMPTS ) {
437+ await pauseExhaustedRecording (
438+ tx ,
439+ videoId ,
440+ job . errorMessage ?? "Automatic processing attempt limit reached." ,
441+ now ,
442+ ) ;
443+ return null ;
444+ }
400445 const attempt : DesktopRecordingAttempt = {
401446 ...job ,
402447 state : job . source ? "processing" : "committing" ,
@@ -651,6 +696,15 @@ export async function scheduleRetry({
651696 . where ( attemptCondition ( fence ) )
652697 . for ( "update" ) ;
653698 if ( ! row ) return false ;
699+ if ( row . attemptCount >= MAX_AUTOMATIC_ATTEMPTS ) {
700+ await pauseExhaustedRecording (
701+ tx ,
702+ fence . videoId ,
703+ `${ errorCode } : ${ errorMessage } ` ,
704+ now ,
705+ ) ;
706+ return true ;
707+ }
654708 await tx
655709 . update ( videoProcessingJobs )
656710 . set ( {
0 commit comments