vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0: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 · 16d 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/server.go +15−14
| @@ -89,20 +89,21 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 89 | 89 | }) | |
| 90 | 90 | lfsStore := lfs.New(cfg.LFSDir(), cfg.LFSObjectMaxBytes) | |
| 91 | 91 | ssh, err := sshserver.New(sshserver.Config{ | |
| 92 | Identity: ident, | ||
| 93 | Repos: rs, | ||
| 94 | LFS: lfsStore, | ||
| 95 | LFSWarnBytes: cfg.LFSWarnBytes, | ||
| 96 | Log: log, | ||
| 97 | HostKeyPath: cfg.SSHHostKey(), | ||
| 98 | MaxBlobBytes: cfg.MaxBlobBytes, | ||
| 99 | AuditPath: cfg.AuditLog(), | ||
| 100 | SpoolDir: cfg.SpoolDir(), | ||
| 101 | RunsDB: cfg.RunsDB(), | ||
| 102 | Runs: runStore, | ||
| 103 | Lease: cfg.Lease(), | ||
| 104 | PagesRef: cfg.PagesRef, | ||
| 105 | PublicURL: cfg.BaseURL(), | ||
| 92 | Identity: ident, | ||
| 93 | Repos: rs, | ||
| 94 | LFS: lfsStore, | ||
| 95 | LFSWarnBytes: cfg.LFSWarnBytes, | ||
| 96 | Log: log, | ||
| 97 | HostKeyPath: cfg.SSHHostKey(), | ||
| 98 | MaxBlobBytes: cfg.MaxBlobBytes, | ||
| 99 | AuditPath: cfg.AuditLog(), | ||
| 100 | SpoolDir: cfg.SpoolDir(), | ||
| 101 | RunsDB: cfg.RunsDB(), | ||
| 102 | Runs: runStore, | ||
| 103 | Lease: cfg.Lease(), | ||
| 104 | MaxJobsPerRepo: cfg.MaxJobsPerRepo, | ||
| 105 | PagesRef: cfg.PagesRef, | ||
| 106 | PublicURL: cfg.BaseURL(), | ||
| 106 | 107 | }) | |
| 107 | 108 | if err != nil { | |
| 108 | 109 | return nil, fmt.Errorf("ssh: %w", err) | |