feat(orchestrator): make the build budget a per-queue setting - #602
Open
behinddwalls wants to merge 1 commit into
Open
feat(orchestrator): make the build budget a per-queue setting#602behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
## Summary
### Why?
How many builds a queue may have occupying CI at once was a constant in the wiring, with a `TODO` beside it saying so:
```go
// TODO: move this onto entity.QueueConfig so operators can tune it per queue
// without a code change.
const defaultBuildBudget = 4
```
Four is a reasonable default and a poor universal answer. It is the only rationing lever the allocator has, and it decides how much speculation a queue does at all: a queue allowed one build never hedges an outcome, and a queue with a large CI pool behind it has no way to say so. A deployment running a busy trunk queue beside a quiet one has to pick a number that suits neither, and changing it means editing Go and shipping a binary.
It is also the setting a reader of the demo asks about first, because it is the one that visibly changes what a run does, and it was the only such knob with no way to set it.
### What?
`profiles.yaml` gains a `speculator` block, per queue and in `defaults`, with one field:
```yaml
defaults:
speculator: {buildBudget: 4}
queues:
- name: demo-queue
speculator: {buildBudget: 12}
```
It inherits and overrides exactly as the other extension blocks do — a queue that says nothing takes the default, and the default itself falls back to 4 when unstated, so every existing configuration and the built-in topology behave as they did.
The block has no `type`. There is one speculator, composed from the queue's scorer, and what varies between queues is what it is allowed to spend — but the block is where an allocator choice would go if a second one ever exists, which a bare `buildBudget:` at queue level would not be.
The `TODO` proposed `entity.QueueConfig` instead. That is the gateway's record of which queues exist; the budget is speculation policy, which is what profiles already carry per queue, and it is resolved a few lines from the scorer it shares a speculator with. `QueueConfig` is left holding just the queue name.
**A negative budget is rejected at startup** rather than clamped. Sticky computes free slots as `budget - funded`, so a negative one yields no free slots ever: the queue would batch and then never build, which reads as a stuck queue rather than a misconfigured one. Absent or `0` takes the default — those are the same value in YAML and cannot be told apart, so the harmless reading wins.
The number is logged alongside the other resolved defaults, since a queue building less than expected is otherwise a silent condition.
## Test Plan
- ✅ a test that drives a real speculator per queue and counts what it proposes — eight dependency-free speculating batches against budgets of 5, 2 (inherited) and 2 (unlisted queue), asserting the proposals stop at the budget. Parsing a number proves nothing if it never reaches the allocator, so the assertion is on behaviour rather than on the parsed config
- ✅ mutation-tested that assertion: reverting `withSpeculator` to the old constant fails all three cases, so it is not passing by construction
- ✅ a negative budget fails `loadProfilesConfig`; an unstated one resolves to 4 while a stated one survives normalization
- ✅ `make test`, `make lint`, `make gazelle`, `make check-tidy`
- ✅ against a live stack, which is what proves the mounted file is read rather than just parsed in a test: `buildBudget: -1` fails the orchestrator at boot with `defaults: build budget -1 is negative`, and `buildBudget: 12` starts, logs `default_build_budget: 12`, and lands a six-change run whose deepest request records `speculating [building ×12, built ×12]` — the raised budget being spent
Sticky's own budget arithmetic is unchanged and already covered; what is new here is only where the number comes from.
behinddwalls
force-pushed
the
preetam/build-budget
branch
from
August 16, 2026 18:46
9ec05f0 to
eac60a9
Compare
behinddwalls
marked this pull request as ready for review
August 16, 2026 18:47
mnoah1
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
How many builds a queue may have occupying CI at once was a constant in the wiring, with a
TODObeside it saying so:Four is a reasonable default and a poor universal answer. It is the only rationing lever the allocator has, and it decides how much speculation a queue does at all: a queue allowed one build never hedges an outcome, and a queue with a large CI pool behind it has no way to say so. A deployment running a busy trunk queue beside a quiet one has to pick a number that suits neither, and changing it means editing Go and shipping a binary.
It is also the setting a reader of the demo asks about first, because it is the one that visibly changes what a run does, and it was the only such knob with no way to set it.
What?
profiles.yamlgains aspeculatorblock, per queue and indefaults, with one field:It inherits and overrides exactly as the other extension blocks do — a queue that says nothing takes the default, and the default itself falls back to 4 when unstated, so every existing configuration and the built-in topology behave as they did.
The block has no
type. There is one speculator, composed from the queue's scorer, and what varies between queues is what it is allowed to spend — but the block is where an allocator choice would go if a second one ever exists, which a barebuildBudget:at queue level would not be.The
TODOproposedentity.QueueConfiginstead. That is the gateway's record of which queues exist; the budget is speculation policy, which is what profiles already carry per queue, and it is resolved a few lines from the scorer it shares a speculator with.QueueConfigis left holding just the queue name.A negative budget is rejected at startup rather than clamped. Sticky computes free slots as
budget - funded, so a negative one yields no free slots ever: the queue would batch and then never build, which reads as a stuck queue rather than a misconfigured one. Absent or0takes the default — those are the same value in YAML and cannot be told apart, so the harmless reading wins.The number is logged alongside the other resolved defaults, since a queue building less than expected is otherwise a silent condition.
Test Plan
withSpeculatorto the old constant fails all three cases, so it is not passing by constructionloadProfilesConfig; an unstated one resolves to 4 while a stated one survives normalizationmake test,make lint,make gazelle,make check-tidybuildBudget: -1fails the orchestrator at boot withdefaults: build budget -1 is negative, andbuildBudget: 12starts, logsdefault_build_budget: 12, and lands a six-change run whose deepest request recordsspeculating [building ×12, built ×12]— the raised budget being spentSticky's own budget arithmetic is unchanged and already covered; what is new here is only where the number comes from.