@@ -195,6 +195,7 @@ const REUSABLE_STARTED_JOB_STATUSES = new Set<RunnerJobStatus>([
195195 "REPAIRING" ,
196196] ) ;
197197const START_CONFLICT_RETRY_LIMIT = 3 ;
198+ const CANCEL_CONFLICT_RETRY_LIMIT = 3 ;
198199const CALLBACK_SETTLEMENT_GRACE_MS = 60_000 ;
199200
200201function jobDeadline ( job : RunnerJob ) : number {
@@ -358,34 +359,37 @@ export class RunnerJobService {
358359 }
359360
360361 async cancelJob ( jobId : string ) : Promise < RunnerJob > {
361- const current = await this . getJob ( jobId ) ;
362- if ( current . status === "CANCELLED" ) return current ;
363- if ( TERMINAL_JOB_STATUSES . has ( current . status ) ) {
364- throw new RunnerCallbackStateError (
365- `Runner job ${ jobId } is already terminal with status ${ current . status } ` ,
366- ) ;
367- }
368- try {
369- return await this . transition (
370- current . jobId ,
371- current . jobVersion ,
372- "CANCELLED" ,
373- {
374- runnerIdentity :
375- current . runnerIdentity ?? "counterlab-control-plane-cancel" ,
376- error : {
377- code : "RUNNER_JOB_CANCELLED" ,
378- message : "The learner cancelled this runner job." ,
379- retryable : true ,
362+ let current = await this . getJob ( jobId ) ;
363+ let lastConflict : ConcurrentRunnerJobUpdateError | undefined ;
364+ for ( let attempt = 0 ; attempt < CANCEL_CONFLICT_RETRY_LIMIT ; attempt += 1 ) {
365+ if ( current . status === "CANCELLED" ) return current ;
366+ if ( TERMINAL_JOB_STATUSES . has ( current . status ) ) {
367+ throw new RunnerCallbackStateError (
368+ `Runner job ${ jobId } is already terminal with status ${ current . status } ` ,
369+ ) ;
370+ }
371+ try {
372+ return await this . transition (
373+ current . jobId ,
374+ current . jobVersion ,
375+ "CANCELLED" ,
376+ {
377+ runnerIdentity :
378+ current . runnerIdentity ?? "counterlab-control-plane-cancel" ,
379+ error : {
380+ code : "RUNNER_JOB_CANCELLED" ,
381+ message : "The learner cancelled this runner job." ,
382+ retryable : true ,
383+ } ,
380384 } ,
381- } ,
382- ) ;
383- } catch ( error ) {
384- if ( ! ( error instanceof ConcurrentRunnerJobUpdateError ) ) throw error ;
385- const updated = await this . getJob ( jobId ) ;
386- if ( updated . status === "CANCELLED" ) return updated ;
387- throw error ;
385+ ) ;
386+ } catch ( error ) {
387+ if ( ! ( error instanceof ConcurrentRunnerJobUpdateError ) ) throw error ;
388+ lastConflict = error ;
389+ current = await this . getJob ( jobId ) ;
390+ }
388391 }
392+ throw lastConflict ?? new ConcurrentRunnerJobUpdateError ( jobId ) ;
389393 }
390394
391395 async expireIfTimedOut ( jobId : string ) : Promise < RunnerJob > {
0 commit comments