Skip to content

Commit 6bc8426

Browse files
committed
feat(entity,storage): rework speculation tree model and store
Reshape the speculation tree data model around the Base/Head path that the build stage actually consumes, and align its store. entity: SpeculationPath{Base, Head} becomes the unit; SpeculationPathInfo carries the path plus its enumerator Score, controller-owned Status (candidate/selected/building/passed/failed/cancelled), and BuildID. Adds SpeculationPathAction and SpeculationPathDecision for the selector seam. SpeculationTree.Speculations becomes Paths. The Build entity drops its own local SpeculationPathInfo and uses the shared SpeculationPath (Head = the batch under verification), with the build controller updated to match. storage: SpeculationTreeStore.UpdateSpeculations(batchID, []SpeculationInfo) becomes Update(ctx, SpeculationTree) — symmetric with Create, keyed by tree.BatchID. MySQL impl and mock updated.
1 parent 3eddc3d commit 6bc8426

7 files changed

Lines changed: 124 additions & 50 deletions

File tree

submitqueue/entity/build.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,6 @@ func (s BuildStatus) IsTerminal() bool {
5353
return s == BuildStatusSucceeded || s == BuildStatusFailed || s == BuildStatusCancelled
5454
}
5555

56-
// SpeculationPathInfo represents the base and head commits of a speculation path used in a build.
57-
type SpeculationPathInfo struct {
58-
// Base is a list of batchIDs(in order) that form the base of this speculation path.
59-
Base []string
60-
}
61-
6256
// Build represents a build scheduled for a batch along a specific speculation path.
6357
// All fields except the Status are immutable after creation.
6458
type Build struct {
@@ -69,8 +63,9 @@ type Build struct {
6963
BatchID string
7064
// SpeculationPath is the speculation path that represents this build. For
7165
// a given batch this path is crafted from the graph that is generated from the
72-
// dependencies of this batch.
73-
SpeculationPath SpeculationPathInfo
66+
// dependencies of this batch. Its Head is the batch being verified (equal to
67+
// BatchID) and its Base is the assumed-good prefix of predecessor batches.
68+
SpeculationPath SpeculationPath
7469
// Score represents the build prediction score for this speculation path.
7570
Score float32
7671
// Status represents the state of the build lifecycle this build is in.

submitqueue/entity/build_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ func TestBuild_ToBytes(t *testing.T) {
7070
build := Build{
7171
ID: "build-1",
7272
BatchID: "batch-1",
73-
SpeculationPath: SpeculationPathInfo{
73+
SpeculationPath: SpeculationPath{
7474
Base: []string{"batch-0", "batch-prev"},
75+
Head: "batch-1",
7576
},
7677
Score: 0.85,
7778
Status: BuildStatusAccepted,
@@ -92,8 +93,9 @@ func TestBuildFromBytes(t *testing.T) {
9293
original := Build{
9394
ID: "build-42",
9495
BatchID: "batch-7",
95-
SpeculationPath: SpeculationPathInfo{
96+
SpeculationPath: SpeculationPath{
9697
Base: []string{"batch-5", "batch-6"},
98+
Head: "batch-7",
9799
},
98100
Score: 0.92,
99101
Status: BuildStatusAccepted,
@@ -145,8 +147,9 @@ func TestBuild_SerializationRoundTrip(t *testing.T) {
145147
build: Build{
146148
ID: "build-100",
147149
BatchID: "batch-50",
148-
SpeculationPath: SpeculationPathInfo{
150+
SpeculationPath: SpeculationPath{
149151
Base: []string{"batch-48", "batch-49"},
152+
Head: "batch-50",
150153
},
151154
Score: 0.75,
152155
Status: BuildStatusAccepted,
@@ -166,8 +169,9 @@ func TestBuild_SerializationRoundTrip(t *testing.T) {
166169
build: Build{
167170
ID: "build-300",
168171
BatchID: "batch-70",
169-
SpeculationPath: SpeculationPathInfo{
172+
SpeculationPath: SpeculationPath{
170173
Base: []string{"batch-65"},
174+
Head: "batch-70",
171175
},
172176
Score: 0,
173177
Status: BuildStatusFailed,

submitqueue/entity/speculation_tree.go

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,112 @@
1414

1515
package entity
1616

17-
// SpeculationPathAction defines the possible actions for a speculation path.
17+
// SpeculationPath is a single speculation path: an assumed-good prefix of
18+
// predecessor batches (Base) on top of which the batch under verification
19+
// (Head) is built and validated.
20+
//
21+
// This is the unit the build stage consumes: Base maps to the build runner's
22+
// base changes (an assumed-good prefix to apply) and Head maps to the changes
23+
// being validated.
24+
type SpeculationPath struct {
25+
// Base is the ordered list of predecessor batch IDs assumed to have passed.
26+
// Empty means the path builds the head batch directly on the target branch.
27+
Base []string
28+
// Head is the batch ID being verified by this path.
29+
Head string
30+
}
31+
32+
// SpeculationPathStatus is the observed lifecycle state of a speculation path.
33+
// It is written only by the orchestrator's speculate controller (into the
34+
// speculation tree store) and read by the path selector as input; enumerators
35+
// and selectors never write it.
36+
type SpeculationPathStatus string
37+
38+
const (
39+
// SpeculationPathStatusUnknown is the unreachable zero value, set by default
40+
// on init. A persisted path always carries a real status (candidate onward),
41+
// so this should never be seen in the store.
42+
SpeculationPathStatusUnknown SpeculationPathStatus = ""
43+
// SpeculationPathStatusCandidate is a freshly enumerated path the controller
44+
// has persisted but not yet sent to build.
45+
SpeculationPathStatusCandidate SpeculationPathStatus = "candidate"
46+
// SpeculationPathStatusSelected is a path the controller has sent to the build
47+
// controller (in response to a selector Build action) but for which no build
48+
// signal has arrived yet — the build system may not have started it
49+
// (resource-gated), so whether it is actually building is not yet known.
50+
SpeculationPathStatusSelected SpeculationPathStatus = "selected"
51+
// SpeculationPathStatusBuilding is a path a build signal has confirmed is in
52+
// flight; its BuildID is known.
53+
SpeculationPathStatusBuilding SpeculationPathStatus = "building"
54+
// SpeculationPathStatusPassed is a path whose build succeeded.
55+
SpeculationPathStatusPassed SpeculationPathStatus = "passed"
56+
// SpeculationPathStatusFailed is a path whose build failed.
57+
SpeculationPathStatusFailed SpeculationPathStatus = "failed"
58+
// SpeculationPathStatusCancelled is a path that is no longer pursued — its
59+
// base was invalidated, its build was cancelled, or the selector dropped it.
60+
SpeculationPathStatusCancelled SpeculationPathStatus = "cancelled"
61+
)
62+
63+
// SpeculationPathAction is the action a path selector asks the controller to
64+
// take for a path. It is the selector's only output: ephemeral (recomputed
65+
// every time the selector runs) and never persisted. The controller enacts it
66+
// and records the resulting SpeculationPathStatus.
1867
type SpeculationPathAction string
1968

2069
const (
21-
// SpeculationPathActionUnknown is the default zero value for SpeculationPathAction.
70+
// SpeculationPathActionUnknown is the unreachable zero value. A real decision
71+
// always carries Build or Cancel; the selector expresses "leave this path
72+
// as-is" by omitting it from its decisions, not by returning this.
2273
SpeculationPathActionUnknown SpeculationPathAction = ""
23-
// TODO: Add comprehensive list of actions
74+
// SpeculationPathActionBuild asks the controller to send this path to the
75+
// build controller (which triggers a build subject to resources). The path moves
76+
// to Selected on send, then Building once a build signal confirms it.
77+
SpeculationPathActionBuild SpeculationPathAction = "build"
78+
// SpeculationPathActionCancel asks the controller to drop this path and
79+
// cancel any build in flight for it.
80+
SpeculationPathActionCancel SpeculationPathAction = "cancel"
2481
)
2582

26-
// SpeculationInfo represents metadata about a single speculation path, including the path through the dependency graph, its current state, and the predicted build score.
27-
type SpeculationInfo struct {
28-
// Path represents the speculation path; which is an ordered list of batches.
29-
Path []string
30-
// Action is a state that this path is in.
31-
Action SpeculationPathAction
32-
// Score is score for this speculation path.
83+
// SpeculationPathInfo is the per-path entry in a speculation tree: a path, the
84+
// enumerator's predicted score for it, its controller-owned status, and a link
85+
// to the build dispatched for it (if any).
86+
type SpeculationPathInfo struct {
87+
// Path is the Base/Head split this entry covers.
88+
Path SpeculationPath
89+
// Score is the enumerator's predicted success score for this path.
3390
Score float32
91+
// Status is the observed lifecycle state of the path. Written only by the
92+
// controller; read by the selector.
93+
Status SpeculationPathStatus
94+
// BuildID links this path to its build. Empty until a build signal confirms
95+
// the build and the controller records it (Selected -> Building); the
96+
// controller never knows the ID at send time.
97+
BuildID string
98+
}
99+
100+
// SpeculationPathDecision is a path selector's decision for a single path: the
101+
// action the controller should take for it. It is the selector's output and is
102+
// not persisted.
103+
type SpeculationPathDecision struct {
104+
// Path identifies the speculation path the action applies to.
105+
Path SpeculationPath
106+
// Action is what the controller should do for the path.
107+
Action SpeculationPathAction
34108
}
35109

36-
// SpeculationTree represents the set of speculation paths constructed for a batch based on its dependency graph.
110+
// SpeculationTree is the set of candidate speculation paths for a batch, built
111+
// from its dependency graph.
37112
type SpeculationTree struct {
38113
// BatchID is the batch for which this speculation tree is constructed.
39114
BatchID string
40-
// Speculations is a list of speculation paths for this batch based on a graph of its
41-
// dependents.
115+
// Paths is the candidate speculation paths for this batch, derived from a
116+
// graph of its dependencies.
42117
//
43118
// For e.g - Consider batches - queueA/batch/1, queueA/batch/2, queueA/batch/3
44119
// such that - queueA/batch/2 and queueA/batch/3 depend on queueA/batch/1
45120
//
46-
// Speculations for queueA/batch/1 - [{Path: []string{"queueA/batch/1"}, State: "scheduled", Score: 0.1}]
47-
// Speculations for queueA/batch/2 - [{Path: []string{"queueA/batch/2"}, State: "scheduled", Score: 0.9}, {Path: []string{"queueA/batch/1", "queueA/batch/2"}, State: "scheduled", Score: 0.3}]
48-
// Speculations for queueA/batch/3 - [{Path: []string{"queueA/batch/3"}, State: "scheduled", Score: 0.9}, {Path: []string{"queueA/batch/1", "queueA/batch/3"}, State: "scheduled", Score: 0.3}]
121+
// Paths for queueA/batch/2 - [{Path: {Base: [], Head: "queueA/batch/2"}, Score: 0.9, Status: "candidate"}, {Path: {Base: ["queueA/batch/1"], Head: "queueA/batch/2"}, Score: 0.3, Status: "candidate"}]
122+
// Paths for queueA/batch/3 - [{Path: {Base: [], Head: "queueA/batch/3"}, Score: 0.9, Status: "candidate"}, {Path: {Base: ["queueA/batch/1"], Head: "queueA/batch/3"}, Score: 0.3, Status: "candidate"}]
49123
//
50-
Speculations []SpeculationInfo
124+
Paths []SpeculationPathInfo
51125
}

submitqueue/extension/storage/mock/speculation_tree_store_mock.go

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

submitqueue/extension/storage/mysql/speculation_tree_store.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (s *speculationTreeStore) Get(ctx context.Context, batchID string) (ret ent
5959
return entity.SpeculationTree{}, fmt.Errorf("failed to get speculation tree entity batchID=%s from the database: %w", batchID, err)
6060
}
6161

62-
if err := json.Unmarshal(speculationsJSON, &st.Speculations); err != nil {
62+
if err := json.Unmarshal(speculationsJSON, &st.Paths); err != nil {
6363
return entity.SpeculationTree{}, fmt.Errorf("failed to unmarshal speculations for speculation tree entity batchID=%s from the database: %w", batchID, err)
6464
}
6565

@@ -71,7 +71,7 @@ func (s *speculationTreeStore) Create(ctx context.Context, speculationTree entit
7171
op := metrics.Begin(s.scope, "create")
7272
defer func() { op.Complete(retErr) }()
7373

74-
speculationsJSON, err := json.Marshal(speculationTree.Speculations)
74+
speculationsJSON, err := json.Marshal(speculationTree.Paths)
7575
if err != nil {
7676
return fmt.Errorf("failed to marshal speculations batchID=%s for Create speculation tree entity: %w", speculationTree.BatchID, err)
7777
}
@@ -91,31 +91,32 @@ func (s *speculationTreeStore) Create(ctx context.Context, speculationTree entit
9191
return nil
9292
}
9393

94-
// UpdateSpeculations updates the speculations of a speculation tree. Returns ErrNotFound if the speculation tree is not found.
95-
func (s *speculationTreeStore) UpdateSpeculations(ctx context.Context, batchID string, speculations []entity.SpeculationInfo) (retErr error) {
96-
op := metrics.Begin(s.scope, "update_speculations")
94+
// Update overwrites the paths of an existing speculation tree, identified by
95+
// speculationTree.BatchID. Returns ErrNotFound if the speculation tree is not found.
96+
func (s *speculationTreeStore) Update(ctx context.Context, speculationTree entity.SpeculationTree) (retErr error) {
97+
op := metrics.Begin(s.scope, "update")
9798
defer func() { op.Complete(retErr) }()
9899

99-
speculationsJSON, err := json.Marshal(speculations)
100+
speculationsJSON, err := json.Marshal(speculationTree.Paths)
100101
if err != nil {
101-
return fmt.Errorf("failed to marshal speculations batchID=%s for UpdateSpeculations: %w", batchID, err)
102+
return fmt.Errorf("failed to marshal paths batchID=%s for Update: %w", speculationTree.BatchID, err)
102103
}
103104

104105
result, err := s.db.ExecContext(ctx,
105106
"UPDATE speculation_tree SET speculations = ? WHERE batch_id = ?",
106-
speculationsJSON, batchID,
107+
speculationsJSON, speculationTree.BatchID,
107108
)
108109
if err != nil {
109-
return fmt.Errorf("failed to update speculations for batchID=%q: %w", batchID, err)
110+
return fmt.Errorf("failed to update speculation tree for batchID=%q: %w", speculationTree.BatchID, err)
110111
}
111112

112113
rowsAffected, err := result.RowsAffected()
113114
if err != nil {
114-
return fmt.Errorf("failed to get rows affected from update for batchID=%q: %w", batchID, err)
115+
return fmt.Errorf("failed to get rows affected from update for batchID=%q: %w", speculationTree.BatchID, err)
115116
}
116117

117118
if rowsAffected != 1 {
118-
return storage.WrapNotFound(fmt.Errorf("speculation tree entity batchID=%s", batchID))
119+
return storage.WrapNotFound(fmt.Errorf("speculation tree entity batchID=%s", speculationTree.BatchID))
119120
}
120121

121122
return nil

submitqueue/extension/storage/speculation_tree_store.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type SpeculationTreeStore interface {
3232
// Returns ErrAlreadyExists if the entry already exists.
3333
Create(ctx context.Context, speculationTree entity.SpeculationTree) error
3434

35-
// UpdateSpeculations updates the speculations of a speculation tree.
36-
// Returns ErrNotFound if the speculation tree is not found.
37-
UpdateSpeculations(ctx context.Context, batchID string, speculations []entity.SpeculationInfo) error
35+
// Update overwrites the paths of an existing speculation tree, identified by
36+
// speculationTree.BatchID. Returns ErrNotFound if the speculation tree is not found.
37+
Update(ctx context.Context, speculationTree entity.SpeculationTree) error
3838
}

submitqueue/orchestrator/controller/build/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (r
143143
build := entity.Build{
144144
ID: buildID.ID,
145145
BatchID: batch.ID,
146-
SpeculationPath: entity.SpeculationPathInfo{Base: append([]string{}, batch.Dependencies...)},
146+
SpeculationPath: entity.SpeculationPath{Base: append([]string{}, batch.Dependencies...), Head: batch.ID},
147147
Status: entity.BuildStatusAccepted,
148148
}
149149

0 commit comments

Comments
 (0)