Skip to content

Commit 315e651

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 iota-based and every type round-trips through ToBytes/FromBytes; covered by table tests. No storage and no wiring — these are the entities the speculation extension and controller build on.
1 parent 4903129 commit 315e651

3 files changed

Lines changed: 552 additions & 0 deletions

File tree

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 = [

submitqueue/entity/speculation.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package entity
16+
17+
import (
18+
"crypto/sha256"
19+
"encoding/hex"
20+
"encoding/json"
21+
"strings"
22+
)
23+
24+
// DependencyBetType is how a path treats one of its head's dependencies.
25+
type DependencyBetType string
26+
27+
const (
28+
// BetUnknown is the zero-value sentinel; it is never a valid bet.
29+
BetUnknown DependencyBetType = ""
30+
// BetIncluded bets the dependency lands: the head is built on top of it, and
31+
// the path is invalidated if the dependency ultimately fails.
32+
BetIncluded DependencyBetType = "included"
33+
// BetExcluded bets the dependency does not land: the head is built without it,
34+
// and the path is invalidated if the dependency ultimately lands.
35+
BetExcluded DependencyBetType = "excluded"
36+
// BetDropped marks a dependency ignored by conflict relaxation: whether it
37+
// lands or fails never affects the path — it neither gates the merge nor
38+
// invalidates the path.
39+
BetDropped DependencyBetType = "dropped"
40+
)
41+
42+
// DependencyBet is a path's bet on one dependency of its head.
43+
type DependencyBet struct {
44+
// Batch is the dependency batch ID this bet is about.
45+
Batch string
46+
// Bet is how the path treats the dependency (included, excluded, or dropped).
47+
Bet DependencyBetType
48+
}
49+
50+
// SpeculationPath is one guess at how a batch's dependencies resolve: a head
51+
// batch plus a bet on each of its dependencies. Every dependency of the head
52+
// appears exactly once, in queue order, so the path is self-describing — its
53+
// full meaning can be read without consulting any external relaxed set or
54+
// dependency list.
55+
type SpeculationPath struct {
56+
// Head is the batch being built along this path.
57+
Head string
58+
// Bets is one bet per dependency of Head, in queue order.
59+
Bets []DependencyBet
60+
}
61+
62+
// ID returns the path's stable identity: a hex-encoded SHA-256 over the head
63+
// and its bets in order. Two paths with the same head and the same ordered
64+
// bets share an ID; any difference in head, dependency, or bet yields a
65+
// different ID.
66+
func (p SpeculationPath) ID() string {
67+
var b strings.Builder
68+
b.WriteString(p.Head)
69+
for _, bet := range p.Bets {
70+
b.WriteByte('\n')
71+
b.WriteString(bet.Batch)
72+
b.WriteByte('=')
73+
b.WriteString(string(bet.Bet))
74+
}
75+
sum := sha256.Sum256([]byte(b.String()))
76+
return hex.EncodeToString(sum[:])
77+
}
78+
79+
// ToBytes serializes the SpeculationPath to JSON bytes for queue message payload.
80+
func (p SpeculationPath) ToBytes() ([]byte, error) {
81+
return json.Marshal(p)
82+
}
83+
84+
// SpeculationPathFromBytes deserializes a SpeculationPath from JSON bytes.
85+
func SpeculationPathFromBytes(data []byte) (SpeculationPath, error) {
86+
var path SpeculationPath
87+
err := json.Unmarshal(data, &path)
88+
return path, err
89+
}
90+
91+
// SpeculationPathStatus is the lifecycle status of one speculation path's
92+
// current build attempt.
93+
type SpeculationPathStatus string
94+
95+
const (
96+
// SpeculationPathStatusUnknown is the zero-value sentinel; it should never be
97+
// seen in the system.
98+
SpeculationPathStatusUnknown SpeculationPathStatus = ""
99+
// SpeculationPathStatusPending indicates the path is funded under the build
100+
// budget but its build has not started yet.
101+
SpeculationPathStatusPending SpeculationPathStatus = "pending"
102+
// SpeculationPathStatusBuilding indicates the path's build is running in the
103+
// build system.
104+
SpeculationPathStatusBuilding SpeculationPathStatus = "building"
105+
// SpeculationPathStatusPassed indicates the path's build completed
106+
// successfully. This is a terminal state.
107+
SpeculationPathStatusPassed SpeculationPathStatus = "passed"
108+
// SpeculationPathStatusFailed indicates the path's build did not complete
109+
// successfully. This is a terminal state.
110+
SpeculationPathStatusFailed SpeculationPathStatus = "failed"
111+
// SpeculationPathStatusCancelling is the non-terminal intent state set when
112+
// the path's build is being cancelled. The build holds its slot until it
113+
// reaches a terminal state.
114+
SpeculationPathStatusCancelling SpeculationPathStatus = "cancelling"
115+
// SpeculationPathStatusCancelled indicates the path's build was cancelled.
116+
// This is a terminal state.
117+
SpeculationPathStatusCancelled SpeculationPathStatus = "cancelled"
118+
)
119+
120+
// IsTerminal returns true if the status represents a final state
121+
// (Passed, Failed, or Cancelled). Cancelling is intentionally excluded: it is
122+
// a non-terminal intent, and the build may still reach Passed or Failed before
123+
// it reaches Cancelled.
124+
func (s SpeculationPathStatus) IsTerminal() bool {
125+
switch s {
126+
case SpeculationPathStatusPassed, SpeculationPathStatusFailed, SpeculationPathStatusCancelled:
127+
return true
128+
default:
129+
return false
130+
}
131+
}
132+
133+
// SpeculationPathEntry is the stored record of one chosen speculation path,
134+
// keyed by the hash of its content. It holds no build reference (that lives on
135+
// the separate execution record, keyed by (ID, Attempt)) and no score (a score
136+
// is meaningful only within a single speculation run).
137+
type SpeculationPathEntry struct {
138+
// ID is the primary key: the hash of the path's content (head plus its
139+
// bets). It equals Path.ID().
140+
ID string
141+
// Path is the head plus one bet per dependency, in queue order.
142+
Path SpeculationPath
143+
// Status is the lifecycle status of the current build attempt.
144+
Status SpeculationPathStatus
145+
// Attempt is the build attempt number for this path, starting at 1. A path
146+
// can be built more than once (e.g. after a prior build is cancelled to free
147+
// budget, or fails and is retried), so ID alone does not identify an
148+
// execution — (ID, Attempt) does. It increments with each new build.
149+
Attempt int
150+
// Version is the version of the object. It is used for optimistic locking.
151+
// Versioning starts at 1 and is incremented for each change to the object.
152+
Version int32
153+
// CreatedAtMs is the creation time in Unix epoch milliseconds.
154+
CreatedAtMs int64
155+
// UpdatedAtMs is the last-update time in Unix epoch milliseconds.
156+
UpdatedAtMs int64
157+
}
158+
159+
// ToBytes serializes the SpeculationPathEntry to JSON bytes for queue message payload.
160+
func (e SpeculationPathEntry) ToBytes() ([]byte, error) {
161+
return json.Marshal(e)
162+
}
163+
164+
// SpeculationPathEntryFromBytes deserializes a SpeculationPathEntry from JSON bytes.
165+
func SpeculationPathEntryFromBytes(data []byte) (SpeculationPathEntry, error) {
166+
var entry SpeculationPathEntry
167+
err := json.Unmarshal(data, &entry)
168+
return entry, err
169+
}
170+
171+
// SpeculationPathSet is one head's chosen speculation paths under a single
172+
// version. It holds both live paths and recently finished ones — finished
173+
// entries linger briefly so that a re-run cannot collide with an old build.
174+
// Every path in the set shares the same head and bets over the same ordered
175+
// dependency list.
176+
type SpeculationPathSet struct {
177+
// BatchID is the primary key: the head batch these paths speculate on.
178+
BatchID string
179+
// Paths is the head's chosen paths, live and recently finished.
180+
Paths []SpeculationPathEntry
181+
// Version is the version of the object. It is used for optimistic locking.
182+
// Versioning starts at 1 and is incremented for each change to the object.
183+
Version int32
184+
}
185+
186+
// ToBytes serializes the SpeculationPathSet to JSON bytes for queue message payload.
187+
func (s SpeculationPathSet) ToBytes() ([]byte, error) {
188+
return json.Marshal(s)
189+
}
190+
191+
// SpeculationPathSetFromBytes deserializes a SpeculationPathSet from JSON bytes.
192+
func SpeculationPathSetFromBytes(data []byte) (SpeculationPathSet, error) {
193+
var set SpeculationPathSet
194+
err := json.Unmarshal(data, &set)
195+
return set, err
196+
}
197+
198+
// PathAction is an action proposed on a speculation path. The set is limited to
199+
// build and cancel; there is no merge or fail action, because a batch's verdict
200+
// is a controller-owned fact, not a proposed action.
201+
type PathAction string
202+
203+
const (
204+
// PathActionUnknown is the zero-value sentinel; it is never a valid action.
205+
PathActionUnknown PathAction = ""
206+
// PathActionBuild proposes starting (or resurrecting) a build for the path.
207+
PathActionBuild PathAction = "build"
208+
// PathActionCancel proposes preempting an in-flight path to free build budget.
209+
PathActionCancel PathAction = "cancel"
210+
)
211+
212+
// Speculation is one proposed action on one path. A path left as-is has no
213+
// Speculation.
214+
type Speculation struct {
215+
// Path is the path the action applies to; its ID hashes the head and its bets.
216+
Path SpeculationPath
217+
// Action is the proposed action (build or cancel).
218+
Action PathAction
219+
}
220+
221+
// CandidatePath is a path paired with the transient ranking score assigned to it
222+
// within a single speculation run. The ranking score orders candidates for that
223+
// run only and is never stored — rankings go stale across runs.
224+
type CandidatePath struct {
225+
// Path is the candidate: a head plus one bet per dependency.
226+
Path SpeculationPath
227+
// RankingScore is the ordering score assigned this run; higher sorts first.
228+
RankingScore float64
229+
}

0 commit comments

Comments
 (0)