autoland: one priority-ordered spawn loop — drain the pipeline before filling it #185
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#185
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
One spawn loop for the whole AFK/autoland fleet, ordered by pipeline stage, replacing the two independent loops that currently race for the same instance cap.
Why
SchedulerLoop(ScheduleOnce) andReaperLoop(AutolandOnce, added by #181) are separate goroutines. Both readliveCount < EffectiveCap(repo)against their own snapshot and both launch. Whichever tick fires first takes the slot, so:#182 makes this strictly worse by adding fix runs as a third spawner. Doing this refactor first means #182 becomes an addition (one new candidate producer) instead of a modification of a racing structure.
The rule
Drain the pipeline before filling it. Work already in flight outranks starting new work, at every stage:
Rationale: finishing in-flight claims drains the pipeline; starting new work fills it. Without the ordering a repo at cap can spend every slot opening new PRs while validated-but-unlanded ones queue up behind them.
Shape
AutolandOncestops launching and becomes a candidate producer.ScheduleOnce's selection likewise becomes a candidate producer for new AFK work.fixrank.Decided along the way
Lander outcomes stop feeding the AFK three-strikes counter. Today
reapRun's failure switch is kind-agnostic (its own doc still enumerates only the AFK kinds, so the coupling looks unintended), which produces two wrong behaviours:consecutive_failures; atPauseThresholdthe repo's AFK auto loop pauses and manual starts 409. Lander flakiness silently suspends unrelated AFK work.rejectclassifiesOutcomeSuccess→ResetRepoFailures. So a lander correctly rejecting a broken AFK PR clears the strikes that broken AFK runs accumulated — the brake gets re-armed by exactly the evidence that should trip it. Under the fix-forward loop this compounds: the pipeline feeds itself.The budget clock stays shared (that part is deliberate and fine); only the failure counter separates. Whether landers get their own counter or simply none is an implementation call — the requirement is that a lander outcome never moves the AFK counter in either direction.
Acceptance criteria
EffectiveCap; no launch decision is made outside itlander > fix > new AFK, pinned by a table test over a mixed candidate setconsecutive_failures; a landerrejectdoes not reset it; AFK run accounting is unchangedfixrankNotes
Landing audit — PR #186
Verdict: PASS.
Verification signal relied on: Forgejo Actions
ci / native— success (6m27s, run 208). Not re-run. Reach confirmed adequate: the hermeticci-nixgate is path-gated to**/*.nix/flake.lock/go.mod/go.sum, none of which this diff touches, so the native gate (Go build + fullgo testwith real git/tmux/prlimit, plus golangci-lint) exercised every changed line.Conventions: title
rework(afk): …is Conventional-Commits shaped; body carries a workingCloses #185; headafk/185matches the claim-branch pattern; branch is up to date withmain(no conflict).Checked beyond CI:
spawnMuis taken only inSpawnOnce;s.muonly inlaunch.go:109/lander.go:127, both reached from inside the pass's launch closures. Order is alwaysspawnMu → mu, never the reverse; no path holdsmuon entry to the pass. No deadlock.AutolandOnce/ScheduleOnceare gone from all Go code; only ADR-0049's historical narrative still names them.ActiveAFKRuns(internal/store/afk.go:26) returns precisely {afk_manual, afk_auto, lander} — plainmanualis excluded — soreapRun's newAFKManual || AFKAutoguard is a clean 'everything reaped except lander', mirroring the existing done-signal guard atreaper.go:324. Nothing that legitimately fed the counter lost its feed.repo/pullare per-iteration under Go 1.22+ semantics (go 1.26).TestSpawnOnce_concurrentReaperAndSchedulerTicksCannotOversubscribeCap(spawn_ac_test.go:104) races the literal tick bodies against cap 1 and asserts bounded (<=1), not-starved (==1), AND that the slot went to the lander.UnderCapremoved from both predicates;spawnPassholds the running count, with the locked per-launch guards as backstop.Non-blocking notes:
trk.Pulls(ctx)— the unbounded history walk PR #179 exists to window — now also fires on the 45s scheduler tick. Costs nothing today:AutolandEnabledis default-off (repos.go:95), so the pre-filter returns nil with zero forge reads.spawnMuis held across a pass's forge reads, so an HTTP toggle-on/reset kick can queue behind an in-flight pass — 'the toggle acts now' is now 'acts after the current pass drains'. Benign (kicks arego-dispatched), but a slight softening of the v0-parity promise the handler comment still states.