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.
docs/roadmap.md +20−5
| @@ -516,16 +516,30 @@ sits inside a signed record and is routinely wrong about the thing a reader woul | |||
| 516 | 516 | it describes is worse than no number. OS and architecture survive the same objection | |
| 517 | 517 | because they describe the machine, which is the fact this slice was actually for. | |
| 518 | 518 | ||
| 519 | ✅ **Slice 10 — one repository may be bounded.** `max_jobs_per_repo` caps how many of | ||
| 520 | one repository's jobs run at once; unset, there is still no cap. The distinction that | ||
| 521 | made this worth a knob: the lease already *orders* by in-flight count, so a burst cannot | ||
| 522 | take the whole fleet — but ordering is not a bound, and what an operator wants when one | ||
| 523 | repository's build saturates the instance is a number. A repository at its cap is | ||
| 524 | *skipped*, not blocking, so the other repositories an account may run are still served | ||
| 525 | and `nil` from the lease now means "work exists, but not for you yet". That second | ||
| 526 | meaning is why `Finish` wakes the parked lease: releasing a slot is work somebody can | ||
| 527 | pick up, and without the wake-up a runner parked on a cap would sit out its whole wait | ||
| 528 | against a queue it could already have. The runners page says `4 of 4 (at the cap)` | ||
| 529 | beside the depth, because a queue that is full and not draining is exactly the question | ||
| 530 | that page exists to answer. | ||
| 531 | (Measured: `TestPerRepoCapBoundsOneRepositoryAndResumesWhenASlotFrees` — two jobs in | ||
| 532 | each of two repositories, cap 1, so "nothing to lease" cannot be confused with "no | ||
| 533 | work": both repositories served once, a third runner refused, a finish wakes the | ||
| 534 | parked lease and the freed slot's own repository is leasable; and | ||
| 535 | `TestRunnersPageSaysWhenARepositoryIsAtItsCap`.) | ||
| 536 | |||
| 519 | 537 | Remaining: | |
| 520 | 538 | ||
| 521 | 539 | - A narrowing filter on the runner (`--repos`, labels) and job-level targeting | |
| 522 | 540 | (`runs-on`): one instance-wide pool is right for identical machines and wrong for one | |
| 523 | 541 | macOS runner in a Linux pool. The filter can only narrow what the allowlist grants, | |
| 524 | 542 | never widen it | |
| 525 | - No per-repository in-flight cap. Fairness is ordering — least busy repository first — | ||
| 526 | which stops a burst taking the whole fleet without a knob to tune; a cap is what an | ||
| 527 | operator wants when they need to *bound* one repository's footprint, and that is a | ||
| 528 | config key to add when somebody asks for the bound rather than the ordering | ||
| 529 | 543 | ||
| 530 | 544 | **Exit criteria:** a push runs a pipeline end-to-end ✅; killing a runner mid-job results in | |
| 531 | 545 | the job being requeued and completing on another runner ✅; a workflow that mutates its | |
| @@ -1023,9 +1037,10 @@ Filled in as phases land. Append; don't rewrite. | |||
| 1023 | 1037 | | 2026-10-23 | "Would this merge?" is answered in memory and surfaced, never gated | `merge-tree --write-tree` merges without a worktree or an index, so the cost is one git process on a page view; gating on it would be wrong because the base can move after an approval, and the roadmap asked for detection before the push rather than a second refusal. The exit status alone is ambiguous — 1 is both "conflicts" and "bad revision" — so the answer is believed only when the output starts with a tree id, and silence otherwise | | |
| 1024 | 1038 | | 2026-10-23 | The queue is reconciled from the refs, not persisted as a push-event log | The roadmap item asked for push events in SQLite, and a row there would not have been atomic with the ref update either: the hook is another process writing another file, so it shrinks the crash window instead of closing it, puts a database write into the path that must never fail a push, and dies with the same `rm data/runs.db` it was meant to survive. A ref tip is already the durable statement that a commit is live, so `ref_cursors` plus a 30-second pass answers the same question without a second copy of the fact — and the spool becomes a latency hint. The cost, accepted and written down: an emptied queue or a first start after an upgrade queues each current tip once | | |
| 1025 | 1039 | | 2026-10-23 | An idle lease parks on an in-process wake-up channel, and eligibility is cached for one poll window | The "long poll" was the client's own timer — the queue answered at once and the runner slept — and every ask re-derived the account's repositories by opening each one's rules, which is O(repos) per idle runner every two seconds. The wait is a hint and never a durability mechanism (the lease is still the database's decision), so a missed wake-up costs one poll interval; in-process is enough because one `vole serve` owns the queue, and a second process would have to ask the database anyway | | |
| 1026 | | 2026-10-23 | A new tip cancels the queued runs of the same ref, and the lease serves the least busy repository first | Pushing twice in a minute built the first commit twice while `canceled` existed with no writer. The cancellation covers queued rows only: interrupting a running build is a different promise from not starting one. Fairness is ordering rather than a per-repository cap, because a cap is a number somebody has to tune and ordering needs none — a burst still runs, it just cannot take every runner | | ||
| 1040 | | 2026-10-23 | A new tip cancels the queued runs of the same ref, and the lease serves the least busy repository first (partly superseded 2026-10-23: ordering is kept for fairness, and the cap is added beside it as `max_jobs_per_repo`) | Pushing twice in a minute built the first commit twice while `canceled` existed with no writer. The cancellation covers queued rows only: interrupting a running build is a different promise from not starting one. Fairness is ordering rather than a per-repository cap, because a cap is a number somebody has to tune and ordering needs none — a burst still runs, it just cannot take every runner | | ||
| 1027 | 1041 | | 2026-10-23 | Runner presence is a queue row per channel, filtered by the clone rule | A runner was an SSH channel invisible to both the web and the queue, so "is my build waiting because nobody is listening" had no answer anywhere. Presence is derived rather than stored: written by connect, lease, heartbeat and finish, dropped when the channel closes, aged out after ten silent minutes. The web is anonymous, so a session appears only while the repository it is building is public — naming a private repository would confirm it exists | | |
| 1028 | 1042 | | 2026-10-23 | A record names the machine (OS, architecture), not the toolchain | The roadmap asked for the runner's Go version too, and it is the one field here that would mislead: `runtime.Version()` reports what compiled the *runner binary*, not what a job builds with, so in a signed record it would be a confident answer to a question nobody asked. OS and architecture describe the machine, which is the fact worth keeping — it is what turns "one macOS runner in a Linux pool" into something a finished run can answer. Both are the binary's build target rather than a live probe: a `uname` subprocess would add a failure path on Windows to sharpen a fact the binary already knows, and the only case it would correct is emulation. The keys are optional so records written before them still read, since a record is never rewritten | | |
| 1043 | | 2026-10-23 | A repository may be bounded with `max_jobs_per_repo`, and finishing a job wakes the lease | Ordering and bounding answer different questions: ordering (least busy repository first) keeps one repository's burst from taking the whole fleet, and a cap is what bounds one repository's footprint on purpose. Enforcing it as a skip in the lease rather than a refusal keeps the other repositories an account may run serviceable, which makes `nil` mean two things — "nothing to do" and "work, but not for you yet" — and a cap-bound queue is exactly where the second meaning would hang forever, since a parked runner has nothing to wake it. So `Finish` notifies as well: a released slot is leasable work. The runners page renders `4 of 4 (at the cap)` for the same reason, because a depth that is full and not draining with no explanation is the question that page exists to answer | | ||
| 1029 | 1044 | ||
| 1030 | 1045 | ## Open questions | |
| 1031 | 1046 | ||