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: one repository may be bounded, and a freed slot wakes the parked

8d33608319bb83725256d02196925309fe521cda · Keenan Nemetz · 17d ago · parent 379dad1e

Phase 4: one repository may be bounded, and a freed slot wakes the parked

max_jobs_per_repo caps how many of one repository's jobs run at once; unset
there is still no cap. Ordering and bounding answer different questions: the
lease already serves the least busy repository first, which stops a burst
taking the whole fleet, but ordering is not a bound — and what an operator
wants when one repository's build saturates the instance is a number.

A repository at its cap is skipped rather than blocking, so the other
repositories an account may run are still served. That makes nil from the
lease mean two things: "nothing to do", and "work exists, but not for you
yet". The second meaning is why Finish now wakes the parked lease — releasing
a slot is work somebody can pick up, and without it a runner parked on a cap
would sit out its whole wait against a queue it could already have taken.
Both are one line of the same query: `? = 0 OR COALESCE(busy.n, 0) < ?`, so
"no cap" does not become a second query string that can drift.

The runners page renders `4 of 4 (at the cap)` beside the depth, because a
queue that is full and not draining is exactly the question that page exists
to answer: "7 waiting, 4 running" is a mystery where the cap is not shown.

Measured: TestPerRepoCapBoundsOneRepositoryAndResumesWhenASlotFrees puts two
jobs in each of two repositories with a cap of 1, so "nothing to lease"
cannot be confused with "no work" — both repositories served once, a third
runner refused, a finish waking the parked lease and the freed slot's own
repository becoming leasable. TestRunnersPageSaysWhenARepositoryIsAtItsCap
and TestMaxJobsPerRepoLoads cover the page and the key.

internal/web/runners.go +10−1

@@ -37,6 +37,11 @@ type queueView struct {
37 37 Name string
38 38 Queued int
39 39 Running int
40 // Cap is the per-repository bound on running jobs, 0 when there is none. It is
41 // shown beside the depth because a queue that is full and not draining is the
42 // question this page exists to answer, and "running 4 of 4" answers it in one
43 // line where "4 running, 7 waiting" leaves the reader guessing.
44 Cap int
40 45 }
41 46
42 47 // RunnersPage serves GET /runners: who is connected, what they are building, and how
@@ -76,12 +81,16 @@ func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) {
76 81 if err != nil {
77 82 w.Logger("runners page: queue depth: %v", err)
78 83 }
84 perRepoCap := 0
85 if w.Cfg != nil {
86 perRepoCap = w.Cfg.MaxJobsPerRepo
87 }
79 88 for name, d := range depths {
80 89 m, err := w.Repos.Open(name)
81 90 if err != nil || !w.public(m) {
82 91 continue
83 92 }
84 queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running})
93 queue = append(queue, queueView{Name: name, Queued: d.Queued, Running: d.Running, Cap: perRepoCap})
85 94 }
86 95 sort.Slice(queue, func(i, j int) bool { return queue[i].Name < queue[j].Name })
87 96 }