Skip to content

Commit bd287ca

Browse files
committed
feat(entity): speculation path and run entities
Add the speculation domain model in submitqueue/entity/speculation.go: SpeculationPath (keyed by a content hash), DependencyBet/DependencyBetType, SpeculationPathStatus, SpeculationPathEntry, and SpeculationPathSet, plus the run vocabulary PathAction/Speculation/CandidatePath. Enums are string-valued with empty-string sentinels; the path's ID is a SHA-256 over the head and its ordered bets, computed on each call. Covered by table tests. Also aligns the speculation RFC with the implemented design: field names match the entities (Head, Batch, string enums), the Generator opens over the batches alone and enumerates the whole path space, terminal-path suppression moves to the Allocator (the piece that reconciles candidates against the path sets by ID), and the stale depth-bound language is removed — lazy generation removed the 2^n cost the bound existed to cap. No storage, no wiring, and no serialization helpers — queue payloads get their own contract types when a message needs one. These are the entities the speculation extension and controller build on.
1 parent 4903129 commit bd287ca

4 files changed

Lines changed: 372 additions & 25 deletions

File tree

doc/rfc/submitqueue/speculation.md

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Verdicts are controller-owned facts: the Speculator can neither compute nor veto
6262

6363
Conflict analysis is conservative — it flags any *possible* conflict — so heads carry dependencies that rarely matter and over-serialize. Relaxation lets the Speculator **drop** the weakest: the path tags that dependency *dropped* and ignores it — it neither gates the merge nor refutes the path if it lands. Which to drop is a per-run Speculator policy.
6464

65-
The drop lives on the path, so the path stays self-describing: finalization needs no external relaxed set, and dropped dependencies don't count toward the depth bound (relaxing is what shrinks a head's depth).
65+
The drop lives on the path, so the path stays self-describing: finalization needs no external relaxed set (relaxing is what shrinks the space of guesses a head's paths range over).
6666

6767
Example: `H` conflicts with `B1` and weak `B2`. Drop `B2`, and `H` merges once `B1` lands and its build passes — even if `B2` later lands. Without it, `H` waits on both.
6868

@@ -95,14 +95,14 @@ The one extension. It decides *which paths to build and which running ones to ca
9595
- `batches`: every in-flight batch plus finalized batches still referenced as dependencies by an in-flight batch; each carries its dependency list and state.
9696
- `pathSets`: every materialized path for those batches, whether pending, in flight, or terminal, including recently finished paths retained to prevent duplicate work.
9797
- **Out:** a list of build/cancel actions whose heads are in `BatchStateSpeculating`; batches in every other state provide facts but are never action targets.
98-
- Budget, depth bound, and clock are injected at construction. An impl may read extra data (also injected); the controller checks the output, so extra data never affects correctness.
98+
- Budget and clock are injected at construction. An impl may read extra data (also injected); the controller checks the output, so extra data never affects correctness.
9999

100100
### The default Speculator
101101

102-
The default Speculator is composed from two swappable interfaces — a **Generator** and an **Allocator** — so scoring and preemption policy can vary independently. They are composition points inside the default implementation, not controller-facing extensions: the controller depends only on the Speculator contract, and an alternate Speculator need not use or expose this split. The default opens the Generator's candidate stream over the batches and their path sets, then hands that stream and the path sets to the Allocator.
102+
The default Speculator is composed from two swappable interfaces — a **Generator** and an **Allocator** — so scoring and preemption policy can vary independently. They are composition points inside the default implementation, not controller-facing extensions: the controller depends only on the Speculator contract, and an alternate Speculator need not use or expose this split. The default opens the Generator's candidate stream over the batches, then hands that stream and the path sets to the Allocator.
103103

104-
- **Generator** — yields the queue's candidate paths as one iterator, best-first across heads in `BatchStateSpeculating`. *Contract:* every candidate has a Speculating head, is coherent, and is within the depth bound (the count of unresolved dependencies a head's paths range over); none repeats or contradicts a resolved fact; a head past the bound is skipped until its dependencies resolve. Ranking is implementation-owned: the Generator may compute it directly, call an injected scorer extension, or use other injected data. It carries the resulting ranking score only within the run and skips paths already terminal in the path sets.
105-
- **Allocator** — spends the build budget (the queue's cap on concurrent builds) over the iterator. *Contract:* it pulls in order until the budget fills and matches candidates to existing paths by ID, so a pending or building path remains funded rather than starting a new attempt; pending dispatches are replayed by the controller as described above. Pending, building, and cancelling paths charge the budget (a cancelling build holds CI until terminal), while terminal ones charge none. Cancellation is best-effort, so the Allocator does not spend capacity it merely expects a cancel to release and risk exceeding the hard CI cap. *Default:* the sticky policy fills only free slots and leaves in-flight builds running; a preempting policy cancels in-flight paths below the funded set. Budget is the only rationing lever — there is no ranking-score floor. A build cancelled to make room still charges budget until its cancel reaches terminal and publishes dirty, so the next run funds the released slot — the queue converges over successive ticks rather than oversubscribing in a single pass.
104+
- **Generator** — yields the queue's candidate paths as one iterator, best-first across heads in `BatchStateSpeculating`. *Contract:* every candidate has a Speculating head and is coherent; none repeats or contradicts a resolved fact. Ranking is implementation-owned: the Generator may compute it directly, call an injected scorer extension, or use other injected data. It carries the resulting ranking score only within the run.
105+
- **Allocator** — spends the build budget (the queue's cap on concurrent builds) over the iterator. *Contract:* it pulls in order until the budget fills and matches candidates to existing paths by ID, so a pending or building path remains funded rather than starting a new attempt, and a candidate whose path is already terminal in the path sets is skipped rather than rebuilt; pending dispatches are replayed by the controller as described above. Pending, building, and cancelling paths charge the budget (a cancelling build holds CI until terminal), while terminal ones charge none. Cancellation is best-effort, so the Allocator does not spend capacity it merely expects a cancel to release and risk exceeding the hard CI cap. *Default:* the sticky policy fills only free slots and leaves in-flight builds running; a preempting policy cancels in-flight paths below the funded set. Budget is the only rationing lever — there is no ranking-score floor. A build cancelled to make room still charges budget until its cancel reaches terminal and publishes dirty, so the next run funds the released slot — the queue converges over successive ticks rather than oversubscribing in a single pass.
106106

107107
### Extension APIs
108108

@@ -123,35 +123,35 @@ type SpeculationPathEntry struct {
123123
// SpeculationPath identifies a head batch and its ordered dependency bets; every
124124
// dependency appears exactly once, so the logical path is self-describing.
125125
type SpeculationPath struct {
126-
HeadBatchID string // ID of the batch being built
127-
Bets []DependencyBet // one bet per dependency of HeadBatchID, in queue order
126+
Head string // ID of the batch being built
127+
Bets []DependencyBet // one bet per dependency of Head, in queue order
128128
}
129129

130130
// DependencyBet is the path's bet on one dependency.
131131
type DependencyBet struct {
132-
BatchID string // dependency batch ID
133-
Bet DependencyBetType // included | excluded | dropped
132+
Batch string // dependency batch ID
133+
Bet DependencyBetType // included | excluded | dropped
134134
}
135135

136136
// DependencyBetType is how a path treats one dependency.
137-
type DependencyBetType int
137+
type DependencyBetType string
138138

139139
const (
140-
BetUnknown DependencyBetType = 0 // zero-value sentinel; never valid
141-
BetIncluded DependencyBetType = 1 // bet it lands; head built on top of it, refuted if it fails
142-
BetExcluded DependencyBetType = 2 // bet it does not land; refuted if it lands
143-
BetDropped DependencyBetType = 3 // dropped by relaxation; landing or failing never affects the path
140+
BetUnknown DependencyBetType = "" // zero-value sentinel; never valid
141+
BetIncluded DependencyBetType = "included" // bet it lands; head built on top of it, refuted if it fails
142+
BetExcluded DependencyBetType = "excluded" // bet it does not land; refuted if it lands
143+
BetDropped DependencyBetType = "dropped" // dropped by relaxation; landing or failing never affects the path
144144
)
145145

146146
// SpeculationPathSet is one head's chosen paths — live and recently finished —
147147
// under a single version. Finished entries linger briefly so a re-run cannot
148148
// collide with an old build; live heads are listed via the batch store's
149149
// by-state query. Every logical path is self-describing, but a store may encode
150150
// the common head and ordered dependency IDs once per set and store each path's
151-
// bets positionally — two bits per dependency, or a base-3 code the depth bound
152-
// keeps to a small integer.
151+
// bets positionally — two bits per dependency, or a base-3 code that stays a
152+
// small integer.
153153
type SpeculationPathSet struct {
154-
BatchID string // primary key; the head batch
154+
Head string // primary key; ID of the head batch
155155
Paths []SpeculationPathEntry // the head's chosen paths
156156
Version int // for compare-and-swap writes
157157
}
@@ -170,17 +170,17 @@ type Speculator interface {
170170
// Speculation is one proposed action on one path; a kept path has no entry.
171171
type Speculation struct {
172172
Path entity.SpeculationPath // the path acted on; its ID hashes head batch ID + its bets
173-
Action PathAction // Build | Cancel
173+
Action PathAction // build | cancel
174174
}
175175

176176
// The only two actions the Speculator may propose. No Merge or Fail —
177177
// verdicts are the controller's.
178-
type PathAction int
178+
type PathAction string
179179

180180
const (
181-
PathActionUnknown PathAction = 0 // zero-value sentinel; never valid, rejected by the controller's check step
182-
Build PathAction = 1 // start (or resurrect) a build for the path
183-
Cancel PathAction = 2 // preempt this in-flight path (refutation cancels are the controller's)
181+
PathActionUnknown PathAction = "" // zero-value sentinel; never valid, rejected by the controller's check step
182+
PathActionBuild PathAction = "build" // start (or resurrect) a build for the path
183+
PathActionCancel PathAction = "cancel" // preempt this in-flight path (refutation cancels are the controller's)
184184
)
185185
```
186186

@@ -189,12 +189,12 @@ const (
189189
// lazily; the producer computes only what is pulled.
190190
type Generator interface {
191191
// Open starts the queue's candidate stream, best-first across Speculating heads.
192-
Open(ctx context.Context, batches []entity.Batch, pathSets []entity.SpeculationPathSet) (PathIterator, error)
192+
Open(ctx context.Context, batches []entity.Batch) (PathIterator, error)
193193
}
194194

195195
type PathIterator interface {
196196
// Next yields the next-best candidate across Speculating heads; ok = false means the
197-
// queue's coherent, depth-capped space is exhausted. Candidates descend in
197+
// queue's coherent space is exhausted. Candidates descend in
198198
// ranking score, never repeat, and never contradict a resolved fact.
199199
Next(ctx context.Context) (c CandidatePath, ok bool, err error)
200200
}
@@ -208,7 +208,8 @@ type CandidatePath struct {
208208

209209
```go
210210
// Allocator — spends the build budget over the iterator, matching in-flight
211-
// paths to the funded set by ID. Budget and clock are injected at construction.
211+
// paths to the funded set by ID and skipping candidates whose path is already
212+
// terminal in the path sets. Budget and clock are injected at construction.
212213
type Allocator interface {
213214
Allocate(ctx context.Context, pathSets []entity.SpeculationPathSet, iter PathIterator) ([]Speculation, error)
214215
}

submitqueue/entity/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ go_library(
2020
"request_history.go",
2121
"request_log.go",
2222
"request_summary.go",
23+
"speculation.go",
2324
],
2425
importpath = "github.com/uber/submitqueue/submitqueue/entity",
2526
visibility = ["//visibility:public"],
@@ -38,6 +39,7 @@ go_test(
3839
"land_test.go",
3940
"request_log_test.go",
4041
"request_test.go",
42+
"speculation_test.go",
4143
],
4244
embed = [":go_default_library"],
4345
deps = [

0 commit comments

Comments
 (0)