11import { CiBuilds } from '../../model/ciBuilds' ;
2+ import { CiJobs } from '../../model/ciJobs' ;
23import { Discord } from '../../service/discord' ;
34import { Dockerhub } from '../../service/dockerhub' ;
45
56export class Cleaner {
67 // Cronjob intentionally has a limited runtime, but it still needs enough
78 // throughput to drain queue backlogs without requiring manual intervention.
89 static readonly maxBuildsProcessedPerRun : number = 25 ;
10+ static readonly activeJobWithoutBuildsAfterMinutes : number = 30 ;
911 static readonly startedBuildPublishProbeAfterMinutes : number = 45 ;
1012 static readonly startedBuildFailureAfterHours : number = 6 ;
1113
1214 static buildsProcessed : number ;
1315
1416 public static async cleanUp ( latestRepoVersion : string ) {
1517 this . buildsProcessed = 0 ;
18+ await this . requeueActiveJobsWithoutBuilds ( ) ;
1619 await this . recoverMaxedOutFailedBuilds ( latestRepoVersion ) ;
1720 await this . reconcileStartedBuildsThatMayHavePublished ( ) ;
1821 await this . cleanUpBuildsThatDidntReportBack ( ) ;
@@ -27,6 +30,36 @@ export class Cleaner {
2730 */
2831 static readonly maxRecoveryAttempts : number = 2 ;
2932
33+ /**
34+ * Jobs can get stuck in "scheduled" or "inProgress" without ever creating a
35+ * build record when the dispatch path flakes before reportNewBuild. Reset
36+ * those jobs back to created so the scheduler can dispatch them again.
37+ */
38+ private static async requeueActiveJobsWithoutBuilds ( ) {
39+ const activeJobs = await CiJobs . getActiveJobs ( ) ;
40+ const staleThresholdMs = this . activeJobWithoutBuildsAfterMinutes * 60 * 1000 ;
41+
42+ for ( const activeJob of activeJobs ) {
43+ if ( this . buildsProcessed >= this . maxBuildsProcessedPerRun ) return ;
44+
45+ const { id : jobId , data : job } = activeJob ;
46+ const lastTouchedSeconds = job . modifiedDate ?. seconds || job . addedDate ?. seconds ;
47+ if ( ! lastTouchedSeconds ) continue ;
48+
49+ const ageMs = Date . now ( ) - lastTouchedSeconds * 1000 ;
50+ if ( ageMs < staleThresholdMs ) continue ;
51+
52+ const hasBuilds = await CiBuilds . hasAnyBuildsForJob ( jobId ) ;
53+ if ( hasBuilds ) continue ;
54+
55+ this . buildsProcessed += 1 ;
56+ await CiJobs . resetJobToCreated ( jobId ) ;
57+ await Discord . sendAlert (
58+ `[Cleaner] Reset stale ${ job . status } job "${ jobId } " back to created because it had no build records after ${ this . activeJobWithoutBuildsAfterMinutes } minutes.` ,
59+ ) ;
60+ }
61+ }
62+
3063 /**
3164 * Automatically recover maxed-out failed builds for the latest repo version.
3265 * If the image is already on DockerHub, mark it as published.
0 commit comments