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/server/runners_page_test.go +35

@@ -10,6 +10,7 @@ import (
10 10 "testing"
11 11 "time"
12 12
13 "github.com/rodent-software/vole/internal/config"
13 14 "github.com/rodent-software/vole/internal/runs"
14 15 )
15 16
@@ -61,6 +62,40 @@ func TestRunnersPageShowsTheRunnerHoldingAJobAndTheQueueDepth(t *testing.T) {
61 62 if strings.Contains(body, "No runner is connected") {
62 63 t.Error("the page says no runner is connected while one is holding a job")
63 64 }
65 // No cap is configured, so the depth is a plain count with nothing to bound it.
66 if strings.Contains(body, " of 1") {
67 t.Errorf("the queue shows a cap that is not configured:\n%s", body)
68 }
69 }
70
71 // A repository at its per-repository cap is the case where a waiting job has a
72 // reason, and the page is where that reason has to be readable: "7 waiting, 4 running"
73 // is a mystery, "4 of 4 (at the cap)" is not.
74 func TestRunnersPageSaysWhenARepositoryIsAtItsCap(t *testing.T) {
75 f := newFixtureWith(t, func(c *config.Config) { c.MaxJobsPerRepo = 1 })
76 f.publish(f.repo)
77 now := time.Now().UTC()
78
79 // One running (leased) and one waiting, with a cap of one.
80 running := queuedRun("01RUNNINGRUN00000000000000", f.repo, "running", now)
81 if _, err := f.srv.Runs.Enqueue(running); err != nil {
82 t.Fatal(err)
83 }
84 if _, err := f.srv.Runs.Lease(f.ownerID, []string{f.repo}, time.Minute, 1, now); err != nil {
85 t.Fatal(err)
86 }
87 waiting := queuedRun("01WAITINGRUN00000000000000", f.repo, "waiting", now)
88 if _, err := f.srv.Runs.Enqueue(waiting); err != nil {
89 t.Fatal(err)
90 }
91
92 code, body := f.get("/runners")
93 if code != 200 {
94 t.Fatalf("GET /runners = %d\n%s", code, body)
95 }
96 if !strings.Contains(body, "1 of 1") || !strings.Contains(body, "at the cap") {
97 t.Fatalf("the page does not say the repository is at its cap of 1:\n%s", body)
98 }
64 99 }
65 100
66 101 func TestRunnersPageKeepsAPrivateRepositoryOut(t *testing.T) {