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) { | |