-
-
Notifications
You must be signed in to change notification settings - Fork 9
fix: add automatic queue self-healing and retry unblocking #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import { Cleaner } from './cleaner'; | ||
| import { RepoVersionInfo } from '../../model/repoVersionInfo'; | ||
|
|
||
| export const cleanUpBuilds = async () => { | ||
| await Cleaner.cleanUp(); | ||
| const latestRepoVersion = await RepoVersionInfo.getLatest(); | ||
| await Cleaner.cleanUp(latestRepoVersion.version); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ export class Scheduler { | |
| private _gitHub: Octokit | undefined; | ||
| private maxConcurrentJobs: number; | ||
| private repoVersionInfo: RepoVersionInfo; | ||
| private reservedRetrySlots = 0; | ||
|
|
||
| private get gitHub(): Octokit { | ||
| // @ts-ignore | ||
|
|
@@ -166,6 +167,7 @@ export class Scheduler { | |
| async ensureThereAreNoFailedJobs(): Promise<boolean> { | ||
| const { maxToleratedFailures, maxExtraJobsForRescheduling } = settings; | ||
| const failingJobs = await CiJobs.getFailingJobsQueue(); | ||
| this.reservedRetrySlots = 0; | ||
|
|
||
| if (failingJobs.length >= 1) { | ||
| const openSpots = await this.determineOpenSpots(); | ||
|
|
@@ -177,14 +179,23 @@ export class Scheduler { | |
| } | ||
|
|
||
| const ingeminator = new Ingeminator(numberToReschedule, this.gitHub); | ||
| await ingeminator.rescheduleFailedJobs(failingJobs); | ||
| const scheduledRetries = await ingeminator.rescheduleFailedJobs(failingJobs); | ||
| this.reservedRetrySlots = Math.min(scheduledRetries, openSpots); | ||
|
|
||
| const remainingFreshSlots = openSpots - this.reservedRetrySlots; | ||
| if (remainingFreshSlots > 0) { | ||
| await Discord.sendDebug( | ||
| `[Scheduler] Reserved ${this.reservedRetrySlots} slot(s) for retries and kept ${remainingFreshSlots} slot(s) available for fresh jobs.`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return failingJobs.length <= maxToleratedFailures; | ||
| const openSpotsAfterRetries = await this.determineOpenSpotsForFreshJobs(); | ||
| return failingJobs.length <= maxToleratedFailures || openSpotsAfterRetries > 0; | ||
|
Comment on lines
+193
to
+194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find callers of ensureThereAreNoFailedJobs to see how the boolean is consumed.
rg -nP --type=ts -C3 '\bensureThereAreNoFailedJobs\s*\('Repository: game-ci/versioning-backend Length of output: 1389 🏁 Script executed: # Search for all references to ensureThereAreNoFailedJobs more broadly
rg -n 'ensureThereAreNoFailedJobs' --type=ts -B2 -A2Repository: game-ci/versioning-backend Length of output: 1058 🏁 Script executed: # Look at the full context of the scheduler function to understand openSpotsAfterRetries
sed -n '160,200p' functions/src/logic/buildQueue/scheduler.tsRepository: game-ci/versioning-backend Length of output: 1811 🏁 Script executed: # Check if there are other files that might reference this function
fd --type=ts -x grep -l 'ensureThereAreNoFailedJobs' {} \;Repository: game-ci/versioning-backend Length of output: 240 🏁 Script executed: # Search for cron-related code
rg -n 'cron\|orchestrat' --type=ts -iRepository: game-ci/versioning-backend Length of output: 52 🏁 Script executed: # Search for scheduler instantiation and other method calls to understand broader context
rg -n 'scheduler\.' --type=ts -C1 | head -100Repository: game-ci/versioning-backend Length of output: 2064 🏁 Script executed: # Search for where scheduleBuildsFromTheQueue is called
rg -n 'scheduleBuildsFromTheQueue' --type=ts -B2 -A2Repository: game-ci/versioning-backend Length of output: 1593 🏁 Script executed: # Look for cloud functions or entry points
rg -n 'export.*function\|exports\.' --type=ts | grep -E '(schedule|build|queue)' | head -20Repository: game-ci/versioning-backend Length of output: 52 🏁 Script executed: # Look at the comment/documentation around ensureThereAreNoFailedJobs more closely
sed -n '160,167p' functions/src/logic/buildQueue/scheduler.tsRepository: game-ci/versioning-backend Length of output: 353 🏁 Script executed: # Check the determineOpenSpotsForFreshJobs method to understand what it counts
rg -n 'determineOpenSpotsForFreshJobs' --type=ts -A10Repository: game-ci/versioning-backend Length of output: 1997 Clarify the intent of the relaxed "healthy" condition. The return condition has been relaxed from strictly While the retry logic still executes before this check, the downstream gate in 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| async buildLatestEditorImages(): Promise<boolean> { | ||
| const openSpots = await this.determineOpenSpots(); | ||
| const openSpots = await this.determineOpenSpotsForFreshJobs(); | ||
| if (openSpots <= 0) { | ||
| await Discord.sendDebug('[Scheduler] Not scheduling any new jobs, as the queue is full'); | ||
| return false; | ||
|
|
@@ -246,4 +257,10 @@ export class Scheduler { | |
| const openSpots = this.maxConcurrentJobs - currentlyRunningJobs; | ||
| return openSpots <= 0 ? 0 : openSpots; | ||
| } | ||
|
|
||
| private async determineOpenSpotsForFreshJobs(): Promise<number> { | ||
| const openSpots = await this.determineOpenSpots(); | ||
| const availableSpots = openSpots - this.reservedRetrySlots; | ||
| return availableSpots <= 0 ? 0 : availableSpots; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -126,6 +126,19 @@ export class CiJobs { | |
| return snapshot.docs.length; | ||
| }; | ||
|
|
||
| static getActiveJobs = async (): Promise<CiJobQueue> => { | ||
| const snapshot = await db | ||
| .collection(CiJobs.collection) | ||
| .where('status', 'in', ['scheduled', 'inProgress']) | ||
| .limit(settings.maxConcurrentJobs) | ||
| .get(); | ||
|
Comment on lines
+129
to
+134
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t cap active-job discovery to scheduler concurrency. Line 133 limits recovery visibility to Suggested fix static getActiveJobs = async (): Promise<CiJobQueue> => {
const snapshot = await db
.collection(CiJobs.collection)
.where('status', 'in', ['scheduled', 'inProgress'])
- .limit(settings.maxConcurrentJobs)
.get();🤖 Prompt for AI Agents |
||
|
|
||
| return snapshot.docs.map((doc) => ({ | ||
| id: doc.id, | ||
| data: doc.data() as CiJob, | ||
| })); | ||
| }; | ||
|
|
||
| static create = async ( | ||
| jobId: string, | ||
| imageType: ImageType, | ||
|
|
@@ -236,6 +249,15 @@ export class CiJobs { | |
| }); | ||
| }; | ||
|
|
||
| static resetJobToCreated = async (jobId: string) => { | ||
| const job = await db.collection(CiJobs.collection).doc(jobId); | ||
|
|
||
| await job.update({ | ||
| status: 'created', | ||
| modifiedDate: Timestamp.now(), | ||
| }); | ||
| }; | ||
|
|
||
| static async removeDryRunJob(jobId: string) { | ||
| if (!jobId.startsWith('dryRun')) { | ||
| throw new Error('Expect only dryRun jobs to be deleted.'); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make stale-job requeue atomic.
Line 52 and Line 56 are separate operations. A late build record can land between them, and the job can be reset to
createdeven though it now has builds.Suggested direction
🤖 Prompt for AI Agents