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
## 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