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/config/config_test.go +33
| @@ -0,0 +1,33 @@ | |||
| 1 | package config | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "os" | ||
| 5 | "path/filepath" | ||
| 6 | "testing" | ||
| 7 | ) | ||
| 8 | |||
| 9 | // A mistyped TOML tag does not fail anything: the key is simply ignored and the | ||
| 10 | // default applies, which is invisible until somebody's cap does nothing. So the one | ||
| 11 | // setting whose absence is a *silent* policy change is pinned here. | ||
| 12 | func TestMaxJobsPerRepoLoads(t *testing.T) { | ||
| 13 | path := filepath.Join(t.TempDir(), "vole.toml") | ||
| 14 | if err := os.WriteFile(path, []byte("max_jobs_per_repo = 3\n"), 0o644); err != nil { | ||
| 15 | t.Fatal(err) | ||
| 16 | } | ||
| 17 | c, err := Load(path) | ||
| 18 | if err != nil { | ||
| 19 | t.Fatal(err) | ||
| 20 | } | ||
| 21 | if c.MaxJobsPerRepo != 3 { | ||
| 22 | t.Fatalf("MaxJobsPerRepo = %d, want 3", c.MaxJobsPerRepo) | ||
| 23 | } | ||
| 24 | |||
| 25 | // Absent means no bound, which is the behaviour before the key existed. | ||
| 26 | unset, err := Load("") | ||
| 27 | if err != nil { | ||
| 28 | t.Fatal(err) | ||
| 29 | } | ||
| 30 | if unset.MaxJobsPerRepo != 0 { | ||
| 31 | t.Fatalf("default = %d, want 0 (no cap)", unset.MaxJobsPerRepo) | ||
| 32 | } | ||
| 33 | } | ||