Skip to content

Commit f299468

Browse files
committed
fix: requeue stale active jobs without builds
1 parent 35fc40b commit f299468

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

functions/src/logic/buildQueue/cleaner.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,57 @@
11
import { CiBuilds } from '../../model/ciBuilds';
2+
import { CiJobs } from '../../model/ciJobs';
23
import { Discord } from '../../service/discord';
34
import { Dockerhub } from '../../service/dockerhub';
45

56
export 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();
1922
await this.healFailedBuildsAlreadyOnDockerHub();
2023
}
2124

25+
/**
26+
* Jobs can get stuck in "scheduled" or "inProgress" without ever creating a
27+
* build record when the dispatch path flakes before reportNewBuild. Reset
28+
* those jobs back to created so the scheduler can dispatch them again.
29+
*/
30+
private static async requeueActiveJobsWithoutBuilds() {
31+
const activeJobs = await CiJobs.getActiveJobs();
32+
const staleThresholdMs = this.activeJobWithoutBuildsAfterMinutes * 60 * 1000;
33+
34+
for (const activeJob of activeJobs) {
35+
if (this.buildsProcessed >= this.maxBuildsProcessedPerRun) return;
36+
37+
const { id: jobId, data: job } = activeJob;
38+
const lastTouchedSeconds = job.modifiedDate?.seconds || job.addedDate?.seconds;
39+
if (!lastTouchedSeconds) continue;
40+
41+
const ageMs = Date.now() - lastTouchedSeconds * 1000;
42+
if (ageMs < staleThresholdMs) continue;
43+
44+
const hasBuilds = await CiBuilds.hasAnyBuildsForJob(jobId);
45+
if (hasBuilds) continue;
46+
47+
this.buildsProcessed += 1;
48+
await CiJobs.resetJobToCreated(jobId);
49+
await Discord.sendAlert(
50+
`[Cleaner] Reset stale ${job.status} job "${jobId}" back to created because it had no build records after ${this.activeJobWithoutBuildsAfterMinutes} minutes.`,
51+
);
52+
}
53+
}
54+
2255
/**
2356
* Automatically recover maxed-out failed builds for the latest repo version.
2457
* If the image is already on DockerHub, mark it as published.

functions/src/model/ciBuilds.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ export class CiBuilds {
106106
}));
107107
};
108108

109+
public static hasAnyBuildsForJob = async (jobId: string): Promise<boolean> => {
110+
const snapshot = await db
111+
.collection(CiBuilds.collection)
112+
.where('relatedJobId', '==', jobId)
113+
.limit(1)
114+
.get();
115+
116+
return snapshot.docs.length > 0;
117+
};
118+
109119
/**
110120
* Registers a new build or handles duplicate dispatches gracefully.
111121
* Returns the existing status if the build is already in progress or published,

functions/src/model/ciJobs.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ export class CiJobs {
126126
return snapshot.docs.length;
127127
};
128128

129+
static getActiveJobs = async (): Promise<CiJobQueue> => {
130+
const snapshot = await db
131+
.collection(CiJobs.collection)
132+
.where('status', 'in', ['scheduled', 'inProgress'])
133+
.limit(settings.maxConcurrentJobs)
134+
.get();
135+
136+
return snapshot.docs.map((doc) => ({
137+
id: doc.id,
138+
data: doc.data() as CiJob,
139+
}));
140+
};
141+
129142
static create = async (
130143
jobId: string,
131144
imageType: ImageType,
@@ -236,6 +249,15 @@ export class CiJobs {
236249
});
237250
};
238251

252+
static resetJobToCreated = async (jobId: string) => {
253+
const job = await db.collection(CiJobs.collection).doc(jobId);
254+
255+
await job.update({
256+
status: 'created',
257+
modifiedDate: Timestamp.now(),
258+
});
259+
};
260+
239261
static async removeDryRunJob(jobId: string) {
240262
if (!jobId.startsWith('dryRun')) {
241263
throw new Error('Expect only dryRun jobs to be deleted.');

0 commit comments

Comments
 (0)