Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions submitqueue/entity/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ go_library(
"request.go",
"request_log.go",
"request_summary.go",
"speculation_path_build.go",
"speculation_tree.go",
],
importpath = "github.com/uber/submitqueue/submitqueue/entity",
Expand All @@ -37,6 +38,7 @@ go_test(
"land_request_test.go",
"request_log_test.go",
"request_test.go",
"speculation_tree_test.go",
],
embed = [":go_default_library"],
deps = [
Expand Down
16 changes: 9 additions & 7 deletions submitqueue/entity/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,18 @@ func (s BuildStatus) IsTerminal() bool {
// Build represents a build scheduled for a batch along a specific speculation path.
// All fields except the Status are immutable after creation.
type Build struct {
// ID represents the build ID. It is the responsibility of a build management system to ensure
// that this is unique.
// ID is the identifier minted by the queue's build runner when the build
Comment thread
behinddwalls marked this conversation as resolved.
// is triggered; this is the primary storage key.
ID string
// BatchID is the batch for which this build is scheduled.
BatchID string
// SpeculationPath is the speculation path that represents this build. For
// a given batch this path is crafted from the graph that is generated from the
// dependencies of this batch. Its Head is the batch being verified (equal to
// BatchID) and its Base is the assumed-good prefix of predecessor batches.
SpeculationPath SpeculationPath
// SpeculationPathID is the ID of the speculation-tree path this build
// verifies (SpeculationPathInfo.ID). The path's structure (Base/Head) is
// not embedded here — it lives on the tree entry and is looked up via the
// tree (SpeculationPathInfo.Path). This field enables the reverse lookup
// from a build row to its path; the forward lookup (path->build) lives in
// the separate SpeculationPathBuild mapping (see speculation_path_build.go).
SpeculationPathID string
// Status represents the state of the build lifecycle this build is in.
Status BuildStatus
}
Expand Down
51 changes: 20 additions & 31 deletions submitqueue/entity/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,10 @@ func TestBuildStatus_IsTerminal(t *testing.T) {

func TestBuild_ToBytes(t *testing.T) {
build := Build{
ID: "build-1",
BatchID: "batch-1",
SpeculationPath: SpeculationPath{
Base: []string{"batch-0", "batch-prev"},
Head: "batch-1",
},
Status: BuildStatusAccepted,
ID: "build-1",
BatchID: "batch-1",
Status: BuildStatusAccepted,
SpeculationPathID: "path-1",
}

data, err := build.ToBytes()
Expand All @@ -86,17 +83,15 @@ func TestBuild_ToBytes(t *testing.T) {
assert.Contains(t, jsonStr, "build-1")
assert.Contains(t, jsonStr, "batch-1")
assert.Contains(t, jsonStr, "accepted")
assert.Contains(t, jsonStr, "path-1")
}

func TestBuildFromBytes(t *testing.T) {
original := Build{
ID: "build-42",
BatchID: "batch-7",
SpeculationPath: SpeculationPath{
Base: []string{"batch-5", "batch-6"},
Head: "batch-7",
},
Status: BuildStatusAccepted,
ID: "build-42",
BatchID: "batch-7",
Status: BuildStatusAccepted,
SpeculationPathID: "path-42",
}

// Serialize
Expand All @@ -110,8 +105,8 @@ func TestBuildFromBytes(t *testing.T) {
// Verify all fields match
assert.Equal(t, original.ID, deserialized.ID)
assert.Equal(t, original.BatchID, deserialized.BatchID)
assert.Equal(t, original.SpeculationPath.Base, deserialized.SpeculationPath.Base)
assert.Equal(t, original.Status, deserialized.Status)
assert.Equal(t, original.SpeculationPathID, deserialized.SpeculationPathID)
}

func TestBuildFromBytes_InvalidJSON(t *testing.T) {
Expand Down Expand Up @@ -139,19 +134,16 @@ func TestBuild_SerializationRoundTrip(t *testing.T) {
build Build
}{
{
name: "accepted build with speculation path",
name: "accepted build with speculation path id",
build: Build{
ID: "build-100",
BatchID: "batch-50",
SpeculationPath: SpeculationPath{
Base: []string{"batch-48", "batch-49"},
Head: "batch-50",
},
Status: BuildStatusAccepted,
ID: "build-100",
BatchID: "batch-50",
Status: BuildStatusAccepted,
SpeculationPathID: "path-100",
},
},
{
name: "succeeded build with no speculation base",
name: "succeeded build with no speculation path id",
build: Build{
ID: "build-200",
BatchID: "batch-60",
Expand All @@ -161,13 +153,10 @@ func TestBuild_SerializationRoundTrip(t *testing.T) {
{
name: "failed build",
build: Build{
ID: "build-300",
BatchID: "batch-70",
SpeculationPath: SpeculationPath{
Base: []string{"batch-65"},
Head: "batch-70",
},
Status: BuildStatusFailed,
ID: "build-300",
BatchID: "batch-70",
Status: BuildStatusFailed,
SpeculationPathID: "path-300",
},
},
}
Expand Down
39 changes: 39 additions & 0 deletions submitqueue/entity/speculation_path_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2025 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package entity

// SpeculationPathBuild is the path->build mapping: given a speculation path's
// ID, it records the build that path resolved to. This is the forward lookup
// (path->build); the reverse lookup (build->path) is Build.SpeculationPathID.
type SpeculationPathBuild struct {
// PathID is the speculation path's ID (SpeculationPathInfo.ID). It is the
// primary key of this mapping and is globally unique — see
// SpeculationPathInfo.ID for the uniqueness contract — so the mapping
// needs no additional scoping key.
PathID string
// BuildID is the runner-minted build ID (Build.ID) this path resolved to.
BuildID string
// BatchID is the batch whose speculation tree contains this path. It
// makes the row self-describing without parsing PathID's internal format.
BatchID string
// CreatedAt is the creation time of this mapping, in milliseconds since
// epoch.
CreatedAt int64
// Version is the version of the object. It is used for optimistic locking:
// updates are conditional on the persisted version matching the caller's
// expected version. Versioning starts at 1; version arithmetic is owned by
// the controller, the store performs a pure conditional write.
Version int32
}
33 changes: 24 additions & 9 deletions submitqueue/entity/speculation_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package entity

import "slices"

// SpeculationPath is a single speculation path: an assumed-good prefix of
// predecessor batches (Base) on top of which the batch under verification
// (Head) is built and validated.
Expand All @@ -29,6 +31,16 @@ type SpeculationPath struct {
Head string
}

// Equal reports whether p and other are structurally the same speculation
// path. It is true iff Head matches and Base has the same elements in the same
// order — Base order is the build order and is significant. The controller uses
// it where only structure can identify a path (deduplicating enumerator output,
// carrying entries over across re-enumeration); everything else references a
// persisted path by its assigned ID (SpeculationPathInfo.ID).
func (p SpeculationPath) Equal(other SpeculationPath) bool {
return p.Head == other.Head && slices.Equal(p.Base, other.Base)
}

// SpeculationPathStatus is the observed lifecycle state of a speculation path.
// It is written only by the orchestrator's speculate controller (into the
// speculation tree store) and read by the decision seams (selector, prioritizer)
Expand Down Expand Up @@ -102,12 +114,15 @@ const (
// entry is persisted; Score, Status, and BuildID are updateable, written only
// by the controller under the tree's Version optimistic lock.
type SpeculationPathInfo struct {
// ID identifies this path within its tree. It is assigned by the controller
// when the path entry is first persisted, immutable thereafter, and unique
// within the tree; its format is the controller's choice and carries no
// meaning — never parse it. Everything outside the tree names a path by this
// ID: seam outputs (path scores, path decisions) and durable links from
// other entities all refer to it rather than restating the Base/Head split.
// ID identifies this path. It is assigned by the controller when the path
// entry is first persisted, immutable thereafter, and globally unique —
// not merely unique within its tree, because other entities key rows by it
// alone (SpeculationPathBuild.PathID is a primary key with no extra
// scoping column). Its format is the controller's choice and carries no
// meaning — never parse it. Everything outside the tree names a path by
// this ID: seam outputs (path scores, path decisions) and durable links
// from other entities all refer to it rather than restating the Base/Head
// split.
ID string
// Path is the Base/Head split this entry covers. Immutable: it identifies
// the entry and never changes after the path is first persisted.
Expand All @@ -124,9 +139,9 @@ type SpeculationPathInfo struct {
// only by the controller; read by the decision seams (scorer, selector,
// prioritizer).
Status SpeculationPathStatus
// BuildID links this path to its build. Updateable: empty until a build
// signal confirms the build and the controller records it (Prioritized ->
// Building); the controller never knows the ID at send time.
// BuildID holds the runner-minted build identifier (also the build store's
// primary key) for this path. Updateable: it is empty until the speculate
// controller's reconcile stamps it once a build exists for this path.
BuildID string
}

Expand Down
75 changes: 75 additions & 0 deletions submitqueue/entity/speculation_tree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2025 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package entity

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSpeculationPath_Equal(t *testing.T) {
tests := []struct {
name string
path SpeculationPath
other SpeculationPath
equal bool
}{
{
name: "equal paths",
path: SpeculationPath{Base: []string{"q/batch/1", "q/batch/2"}, Head: "q/batch/3"},
other: SpeculationPath{Base: []string{"q/batch/1", "q/batch/2"}, Head: "q/batch/3"},
equal: true,
},
{
name: "different head",
path: SpeculationPath{Base: []string{"q/batch/1"}, Head: "q/batch/2"},
other: SpeculationPath{Base: []string{"q/batch/1"}, Head: "q/batch/3"},
equal: false,
},
{
name: "different base order",
path: SpeculationPath{Base: []string{"q/batch/1", "q/batch/2"}, Head: "q/batch/3"},
other: SpeculationPath{Base: []string{"q/batch/2", "q/batch/1"}, Head: "q/batch/3"},
equal: false,
},
{
name: "different base length",
path: SpeculationPath{Base: []string{"q/batch/1"}, Head: "q/batch/3"},
other: SpeculationPath{Base: []string{"q/batch/1", "q/batch/2"}, Head: "q/batch/3"},
equal: false,
},
{
name: "both empty",
path: SpeculationPath{},
other: SpeculationPath{},
equal: true,
},
{
name: "nil base equals empty base",
path: SpeculationPath{Base: nil, Head: "q/batch/1"},
other: SpeculationPath{Base: []string{}, Head: "q/batch/1"},
equal: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.equal, tt.path.Equal(tt.other))
// Equal must be symmetric.
assert.Equal(t, tt.equal, tt.other.Equal(tt.path))
})
}
}
1 change: 1 addition & 0 deletions submitqueue/extension/storage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"change_store.go",
"request_log_store.go",
"request_store.go",
"speculation_path_build_store.go",
"speculation_tree_store.go",
"storage.go",
],
Expand Down
1 change: 1 addition & 0 deletions submitqueue/extension/storage/mock/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"change_store_mock.go",
"request_log_store_mock.go",
"request_store_mock.go",
"speculation_path_build_store_mock.go",
"speculation_tree_store_mock.go",
"storage_mock.go",
],
Expand Down
Loading
Loading