Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 4: a new tip cancels the old build, and the lease serves the quiet repo

760b523a7dbb6fc1c1210a16bf44232a7c592b1b · Keenan Nemetz · 17d ago · parent 34afc569

Phase 4: a new tip cancels the old build, and the lease serves the quiet repo

Two ways a single repository could monopolise the instance's runner pool. A push
series queued a build per commit, so ten pushes in a minute ran ten builds of nine
dead commits — `StatusCanceled` existed, the gate knew how to report it, and nothing
ever wrote it. And the lease ordered by age across every eligible repository, so the
oldest job won wherever it lived: one busy repository's queue was served before a
quiet repository's, no matter how many runners were busy with the first.

Cancellation is now decided where the workflow's own trigger is decided: a push that
owes at least one run retires the *queued* runs of that repo+ref whose sha it
replaces, and nothing else — a running job is never interrupted, since a build
canceled mid-flight is a different promise from one nobody started, and a push that
queues nothing (no matching workflow) cancels nothing. The count is logged, because a
run that stops for no findable reason reads exactly like a run that vanished.

The lease orders by in-flight count per repository first and age within a repository
second, which needs no new config key and no operator tuning: the burst still runs,
it just cannot occupy every runner while another repository waits.

Measured: TestCancelSupersededOnlyTouchesWaitingRunsOfTheSameRef (1 canceled, another
ref untouched, the running job still running), TestLeaseServesTheLeastBusyRepoFirst
(the busy repository's older second job is passed over for the quiet repository), and
TestANewerTipCancelsTheQueuedBuildOfTheOlderOne end to end through the reconciler.

Canceled runs are queue state, not history — they never happened, so they have no
record and `vole index rebuild` drops them rather than inventing one.

internal/server/reconcile_test.go +30−1

@@ -2,6 +2,7 @@
2 2 package server
3 3
4 4 import (
5 "fmt"
5 6 "io"
6 7 "log/slog"
7 8 "os"
@@ -19,6 +20,7 @@ import (
19 20 // queue. Everything else the Server holds is for serving, not for deciding what work
20 21 // is owed.
21 22 type reconcileHarness struct {
23 n int
22 24 s *Server
23 25 repo *repo.Meta
24 26 runs *runs.Store
@@ -87,10 +89,11 @@ func (h *reconcileHarness) commit(t *testing.T, ref, workflow string) string {
87 89 if err != nil {
88 90 t.Fatal(err)
89 91 }
90 commit, err := g.CommitTree(root, "", "work")
92 commit, err := g.CommitTree(root, "", fmt.Sprintf("work %d", h.n))
91 93 if err != nil {
92 94 t.Fatal(err)
93 95 }
96 h.n++
94 97 if err := g.UpdateRef(ref, commit); err != nil {
95 98 t.Fatal(err)
96 99 }
@@ -180,6 +183,32 @@ func TestReconcileIgnoresTheMetadataBranch(t *testing.T) {
180 183 }
181 184 }
182 185
186 // Pushing twice must not leave the first commit's build queued behind the second's:
187 // a new tip cancels what was waiting for the old one.
188 func TestANewerTipCancelsTheQueuedBuildOfTheOlderOne(t *testing.T) {
189 h := newReconcileHarness(t)
190 first := h.commit(t, "refs/heads/main", testWorkflow)
191 h.s.reconcileRefs()
192
193 second := h.commit(t, "refs/heads/main", testWorkflow)
194 h.s.reconcileRefs()
195
196 bySHA := map[string]string{}
197 list, err := h.runs.Recent("demo", 10)
198 if err != nil {
199 t.Fatal(err)
200 }
201 for _, r := range list {
202 bySHA[r.SHA] = r.Status
203 }
204 if got := bySHA[first]; got != runs.StatusCanceled {
205 t.Fatalf("the run for the replaced tip is %q, want %q", got, runs.StatusCanceled)
206 }
207 if got := bySHA[second]; got != runs.StatusQueued {
208 t.Fatalf("the run for the new tip is %q, want %q", got, runs.StatusQueued)
209 }
210 }
211
183 212 // A deleted branch drops its cursor, so the branch coming back at a new tip is work
184 213 // owed rather than work suppressed by a note about a branch that no longer exists.
185 214 func TestReconcileDropsTheCursorOfADeletedBranch(t *testing.T) {