You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(orchestrator): make the build budget a per-queue setting (#602)
## 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.
Copy file name to clipboardExpand all lines: doc/howto/QUICKSTART.md
+16-1Lines changed: 16 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,21 @@ Every change writes all of its files into one folder under `demo/`, and `FOLDERS
70
70
71
71
Set it deliberately when you want a run to show one thing. `FOLDERS=1` puts every change in the same place, so the queue serializes the lot and each change speculates on the one before it. A number well above `COUNT` keeps them all apart, so they go out together.
72
72
73
+
How much speculation that turns into is capped by the queue's **build budget** — how many builds it may have occupying CI at once, counted across every in-flight batch rather than per batch. It defaults to 4 and is set per queue in the provider's `profiles.yaml`:
74
+
75
+
```yaml
76
+
defaults:
77
+
speculator: {buildBudget: 4}
78
+
79
+
queues:
80
+
- name: demo-queue
81
+
speculator: {buildBudget: 12}
82
+
```
83
+
84
+
It is the other half of `FOLDERS`. Folders decide how many dependencies there are to speculate *about*; the budget decides how many of the possible outcomes the queue may hedge at once. `FOLDERS=1 buildBudget: 1` explores one path at a time and lands the slowest; raising the budget lets the queue build the "it fails" branch alongside the "it succeeds" one, which is what makes a failure cost nothing. A trail like `speculating [building ×8, built ×8]` below is a queue that kept finding paths worth funding.
85
+
86
+
Changing it needs a restart, since the file is read at startup — `make local-submitqueue-stop && make local-submitqueue-start`.
Eight builds means the batch was speculating down eight paths at once, and `waiting` means one of them passed and then sat on a dependency that had not resolved. A request that sailed through reads `speculating [building, built]` instead — the same position, a very different amount of work behind it.
126
+
Eight builds means the batch explored eight paths before one of them landed it — not eight at the same time, since the build budget above caps how many may hold CI at once and a finished build frees its slot for the next. `waiting` means a path passed and then sat on a dependency that had not resolved. A request that sailed through reads `speculating [building, built]` instead — the same position, a very different amount of work behind it.
112
127
113
128
`land-watch` fixes its set when it starts and exits non-zero if any request in that set finishes anywhere other than `landed`, which makes it usable from a script. A request accepted after the watch begins is not picked up: a watch that grew as the queue did would never finish.
Copy file name to clipboardExpand all lines: service/submitqueue/demo/provider/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ Each directory here is one **provider** — a code-hosting system SubmitQueue la
4
4
5
5
| File | Selects |
6
6
|---|---|
7
-
|`profiles.yaml`| the change provider, build runner, and conflict analyzer each queue resolves to (read by the orchestrator) |
7
+
|`profiles.yaml`| the change provider, build runner, conflict analyzer, scorer and build budget each queue resolves to (read by the orchestrator) |
8
8
|`merge.yaml`| the merge target each queue lands on (read by Runway) |
9
9
10
10
Neither holds a secret. Each integration names the *environment variable* carrying its credential, so these files stay committable and rotating a token needs no edit.
0 commit comments