Skip to content
Open
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
4 changes: 4 additions & 0 deletions submitqueue/entity/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ go_library(
"land_request.go",
"merge_result.go",
"push_result.go",
"queue.go",
"queue_config.go",
"request.go",
"request_log.go",
"speculation_path_build.go",
"speculation_tree.go",
],
importpath = "github.com/uber/submitqueue/submitqueue/entity",
Expand All @@ -34,8 +36,10 @@ go_test(
"build_test.go",
"cancel_request_test.go",
"land_request_test.go",
"queue_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
// 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
37 changes: 37 additions & 0 deletions submitqueue/entity/queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 "encoding/json"

// QueueID is the queue-message payload for queue-scoped pipeline stages. It
// carries only the queue name; consumers resolve the state they need from
// storage.
type QueueID struct {
// Name is the merge-queue name the message targets.
Name string `json:"name"`
}

// ToBytes serializes the QueueID to JSON bytes for queue message payload.
func (q QueueID) ToBytes() ([]byte, error) {
return json.Marshal(q)
}

// QueueIDFromBytes deserializes a QueueID from JSON bytes.
func QueueIDFromBytes(data []byte) (QueueID, error) {
var qid QueueID
err := json.Unmarshal(data, &qid)
return qid, err
}
72 changes: 72 additions & 0 deletions submitqueue/entity/queue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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"
"github.com/stretchr/testify/require"
)

func TestQueueID_SerializationRoundTrip(t *testing.T) {
tests := []struct {
name string
queueID QueueID
}{
{
name: "simple queue name",
queueID: QueueID{Name: "queueA"},
},
{
name: "another queue name",
queueID: QueueID{Name: "queueB"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.queueID.ToBytes()
require.NoError(t, err)

deserialized, err := QueueIDFromBytes(data)
require.NoError(t, err)

assert.Equal(t, tt.queueID, deserialized)
})
}
}

func TestQueueIDFromBytes_InvalidJSON(t *testing.T) {
_, err := QueueIDFromBytes([]byte(`{"invalid": json"}`))
assert.Error(t, err)
}

func TestQueueIDFromBytes_EmptyJSON(t *testing.T) {
queueID, err := QueueIDFromBytes([]byte(`{}`))
require.NoError(t, err)

assert.Empty(t, queueID.Name)
}

func TestQueueIDFromBytes_EmptyBytes(t *testing.T) {
_, err := QueueIDFromBytes([]byte{})
assert.Error(t, err)
}

func TestQueueIDFromBytes_NilBytes(t *testing.T) {
_, err := QueueIDFromBytes(nil)
assert.Error(t, err)
}
43 changes: 43 additions & 0 deletions submitqueue/entity/speculation_path_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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. It is written by the build
// controller when it triggers a build for a speculation path; this is the
// forward lookup (path->build), while the reverse lookup (build->path) is
// Build.SpeculationPathID. Today a row is write-once — only Create and Get
// exist, no Update — so Version is always 1 on every row; the field is
// reserved for a future repair/re-trigger flow that conditionally re-points a
// path to a newer build without a schema migration (not used yet).
type SpeculationPathBuild struct {
// PathID is the speculation path's ID (SpeculationPathInfo.ID). It is the
// primary key of this mapping.
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
// Version is the version of the object, 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. Not
// used yet — reserved for a future conditional-update flow.
Version int32
// CreatedAt is the creation time of this mapping, in milliseconds since
// epoch.
CreatedAt int64
}
36 changes: 30 additions & 6 deletions submitqueue/entity/speculation_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ type SpeculationPath struct {
Head string
}

// Equal reports whether p and other are the same speculation path: this is the
// identity used to match Build records to tree paths. 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.
func (p SpeculationPath) Equal(other SpeculationPath) bool {
if p.Head != other.Head {
return false
}
if len(p.Base) != len(other.Base) {
return false
}
for i := range p.Base {
if p.Base[i] != other.Base[i] {
return false
}
}
return true
}

// 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 @@ -98,10 +117,15 @@ const (

// SpeculationPathInfo is the per-path entry in a speculation tree: a path, its
// latest predicted-success score, its controller-owned status, and a link to
// the build dispatched for it (if any). Path is immutable once the entry is
// persisted; Score, Status, and BuildID are updateable, written only by the
// controller under the tree's Version optimistic lock.
// the build dispatched for it (if any). ID and Path are immutable once the
// 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 minted by the controller
// at tree creation, immutable thereafter, and unique within the tree; its
// format is the controller's choice. It is the key of the path->build
// mapping row (SpeculationPathBuild.PathID).
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.
Path SpeculationPath
Expand All @@ -117,9 +141,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
Loading
Loading