Auto-Recover Stuck Queue Builds And Fix Queue Status API - #96
Conversation
📝 WalkthroughWalkthroughRefactoring removes global RepoVersionInfo dependency from Ingeminator and instead retrieves version info per-job; adds maxed-out failed build recovery to Cleaner with DockerHub checks; extends CiBuilds with repo-version-scoped queries; integrates recovery into the scheduler; and updates API endpoints to use new query capabilities. ChangesBuild Queue Refactoring and Recovery
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Ingeminator: guard against null meta.lastBuildFailure (epoch fallback) so a recovered build cannot crash the scheduler tick on the next pass. - ciBuilds.resetFailureCount: write Timestamp.fromMillis(0) instead of null for the same reason (defence in depth). - Cleaner.recoverMaxedOutFailedBuilds: cap to maxBuildsProcessedPerRun per tick, track meta.recoveryCount, and alert + stop resetting after maxRecoveryAttempts so genuinely broken builds cannot loop forever. - firestore.indexes.json: composite index for (status, buildInfo.repoVersion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
queueStatuscross-origin access for the docs admin UIretryBuildto use the job's repo version instead of the latestProblem
The docs site calls
queueStatuscross-origin fromgame.ci. Without CORS headers the browser fails the request withFailed to fetch, which breaks Admin Queue Management.Separately, retry-exhausted failed builds required a manual
resetFailedBuildscall before the queue could retry them again. Because the scheduler prioritises failed jobs before newcreatededitor jobs, that left newer editor versions stuck behind stale failures and required admin intervention. We saw this on repo version 3.2.2 (Total 7108, Published 7099, In progress 0, ~9 failed) where created jobs for newer editors were showing the builder-head icon on the docs site indefinitely.Changes
OPTIONShandling toqueueStatusqueueStatus(jobs remain global, builds are filtered)retryBuild: dispatch with the build's ownjobData.repoVersionInforather thanRepoVersionInfo.getLatest(), so retrying an older repo version no longer mislabels the dispatch as the current latesthealFailedBuildsAlreadyOnDockerHubheuristic, but applied to the maxed-out subset)Recovery safety caps (why)
First-pass review caught three failure modes in the naive recovery path; this PR addresses each:
resetFailureCountpreviously wrotemeta.lastBuildFailure = null, butIngeminator.rescheduleFailedBuildsForJobreads it aslastFailure.toMillis()for backoff computation. Once auto-recovery runs every tick, that null deref would crash the scheduler pass before any new editor jobs could be scheduled — i.e. the fix would deadlock the very thing it was meant to unblock. Fixed by (a) writingTimestamp.fromMillis(0)instead ofnullinresetFailureCountand (b) guarding the Ingeminator withlastBuildFailure ?? Timestamp.fromMillis(0). Defence in depth — the same null path was reachable from the existing admin reset endpoint.maxFailuresPerBuild(15) times, get auto-reset, fail another 15 times, get reset again, forever — burning Actions minutes and Docker Hub API quota with no escalation. Addedmeta.recoveryCount(tracked viaCiBuilds.incrementRecoveryCount) andCleaner.maxRecoveryAttempts = 2. After two recovery rounds the build is left at max retries and an alert goes to Discord asking for manual investigation.maxBuildsProcessedPerRun(5) per tick; the rest are taggeddeferred:in the returned summary and picked up next tick.Order of operations in
scheduleBuildsFromTheQueueCiJobs.markJobsBeforeRepoVersionAsSuperseded— old-repo jobs no longer block the failing-jobs queueCleaner.recoverMaxedOutFailedBuilds— maxed-out builds either becomepublished(if on DockerHub) or have their failure count reset for the IngeminatorSchedulerruns as before: base image, hub image,ensureThereAreNoFailedJobs(Ingeminator), thenbuildLatestEditorImagesRecovery runs before the Ingeminator on purpose, so by the time the Ingeminator iterates failed builds, no entry is at max retries.
Firestore index
Added a composite index
(status ASC, buildInfo.repoVersion ASC)onciBuildsfor the newgetMaxedOutFailedBuildsForRepoVersion/getAllForRepoVersionqueries. Without it the first production call would fail with a "create this index" URL.Known limitations / follow-ups
supersededso they do not block scheduling.'failed'until all its builds reach'published'. If the failing-job count exceedsmaxToleratedFailures(2),ensureThereAreNoFailedJobsstill short-circuits beforebuildLatestEditorImages. This PR fixes the retry-exhausted sub-case; clearing backlogs of healthy-but-failing jobs is still the Ingeminator's responsibility.specificTag/friendlyTagwritten bymarkBuildAsPublishedfrom recovery are${baseOs}-${repoVersion}and the major.minor ofrepoVersionrespectively. These do not include the editor version / target platform. This matches the existinghealFailedBuildsAlreadyOnDockerHubandcleanUpBuildsThatDidntReportBackcleaners — i.e. an existing inconsistency, worth a follow-up to normalise across all three call sites.queueStatusis now world-readable with no auth. Response includes job IDs, statuses and DockerHub digests; write paths (resetFailedBuilds,retryBuild) remain auth-gated. Intentional, called out here for visibility.Testing
yarn typecheck