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
1 change: 1 addition & 0 deletions submitqueue/entity/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ go_library(
"land.go",
"list.go",
"merge_result.go",
"path_build.go",
"push_result.go",
"queue_config.go",
"request.go",
Expand Down
13 changes: 13 additions & 0 deletions submitqueue/entity/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,25 @@ 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.
//
// It is keyed by the runner's build ID, which is the identifier every stage
// downstream of the trigger already holds: a poll, a webhook, and a runner-side
// log line all name a build, none of them names a speculation path. The path
// coordinates ride along on the record so those stages never have to
// understand speculation to do their job.
type Build struct {
// 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
// PathID is the speculation path this build verifies, as carried by
// SpeculationPathEntry.ID.
PathID string
// Attempt is which build attempt for that path this is, starting at 1.
// A path may be built more than once, so ID names the run while
// (PathID, Attempt) names the slot it occupies.
Attempt int
// Status represents the state of the build lifecycle this build is in.
Status BuildStatus
}
Expand Down
35 changes: 35 additions & 0 deletions submitqueue/entity/path_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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

// PathBuild names the build started for one attempt of one speculation path.
//
// It is the reverse of Build's key. A Build is keyed by the identifier the
// runner minted, which is what every stage watching a build already holds; a
// caller starting from a path has no way to derive that identifier, so the link
// is recorded under the coordinates it does hold.
//
// A record is write-once: it is created already naming its build and never
// changes, so an attempt maps to one build for good — a retried path is a new
// attempt under a different key. An absent record means no build is recorded
// for the attempt; it does not promise that none is starting.
type PathBuild struct {
// PathID is the speculation path, as carried by SpeculationPathEntry.ID.
PathID string
// Attempt is which build attempt for that path this is, starting at 1.
Attempt int
// BuildID is the build started for that attempt. Never empty.
BuildID string
}
25 changes: 22 additions & 3 deletions submitqueue/entity/speculation.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ func (p SpeculationPath) ID() string {
return hex.EncodeToString(sum[:])
}

// Base returns the path's base — the batches its head is stacked on top of:
// the IDs of the dependencies the path assumes will succeed, in the path's
// dependency order.
//
// It is a projection of the path rather than a decision about it — a
// dependency the path assumes will fail is by definition built without, and
// an ignored one is not built on either — so every caller that needs the base
// derives it here rather than re-reading the assumptions itself.
func (p SpeculationPath) Base() []string {
var deps []string
for _, dep := range p.Dependencies {
if dep.Assumption == DependencyAssumptionSucceeds {
deps = append(deps, dep.Batch)
}
}
return deps
}

// SpeculationPathStatus is the lifecycle status of one speculation path's
// current build attempt.
type SpeculationPathStatus string
Expand Down Expand Up @@ -122,9 +140,10 @@ func (s SpeculationPathStatus) IsTerminal() bool {
}

// SpeculationPathEntry is the stored record of one chosen speculation path,
// keyed by the hash of its content. It holds no build reference (that lives on
// the separate execution record, keyed by (ID, Attempt)) and no score (a score
// is meaningful only within a single speculation run).
// keyed by the hash of its content. It holds no build reference — a build is
// linked to an attempt by PathBuild, so the path stays what the speculation run
// decided rather than a mirror of what the build system is doing — and no score
// (a score is meaningful only within a single speculation run).
type SpeculationPathEntry struct {
// ID is the primary key: the hash of the path's content (head plus its
// assumptions). It always equals Path.ID() — it is materialized here, rather
Expand Down
1 change: 1 addition & 0 deletions submitqueue/extension/storage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go_library(
"batch_store.go",
"build_store.go",
"change_store.go",
"path_build_store.go",
"request_batch_store.go",
"request_log_store.go",
"request_queue_summary_store.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 @@ -7,6 +7,7 @@ go_library(
"batch_store_mock.go",
"build_store_mock.go",
"change_store_mock.go",
"path_build_store_mock.go",
"request_batch_store_mock.go",
"request_log_store_mock.go",
"request_queue_summary_store_mock.go",
Expand Down
71 changes: 71 additions & 0 deletions submitqueue/extension/storage/mock/path_build_store_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions submitqueue/extension/storage/mock/storage_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions submitqueue/extension/storage/mysql/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go_library(
"batch_store.go",
"build_store.go",
"change_store.go",
"path_build_store.go",
"request_batch_store.go",
"request_log_store.go",
"request_queue_summary_store.go",
Expand Down
12 changes: 6 additions & 6 deletions submitqueue/extension/storage/mysql/build_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func (s *buildStore) Get(ctx context.Context, id string) (ret entity.Build, retE
var build entity.Build

err := s.db.QueryRowContext(ctx,
"SELECT id, batch_id, status FROM build WHERE id = ?",
"SELECT id, batch_id, path_id, attempt, status FROM build WHERE id = ?",
id,
).Scan(&build.ID, &build.BatchID, &build.Status)
).Scan(&build.ID, &build.BatchID, &build.PathID, &build.Attempt, &build.Status)

if errors.Is(err, sql.ErrNoRows) {
return entity.Build{}, storage.WrapNotFound(err)
Expand All @@ -66,8 +66,8 @@ func (s *buildStore) Create(ctx context.Context, build entity.Build) (retErr err
defer func() { op.Complete(retErr) }()

_, err := s.db.ExecContext(ctx,
"INSERT INTO build (id, batch_id, status) VALUES (?, ?, ?)",
build.ID, build.BatchID, build.Status,
"INSERT INTO build (id, batch_id, path_id, attempt, status) VALUES (?, ?, ?, ?, ?)",
build.ID, build.BatchID, build.PathID, build.Attempt, build.Status,
)
if err != nil {
var mysqlErr *mysql.MySQLError
Expand All @@ -86,8 +86,8 @@ func (s *buildStore) Update(ctx context.Context, build entity.Build) (retErr err
defer func() { op.Complete(retErr) }()

result, err := s.db.ExecContext(ctx,
"UPDATE build SET batch_id = ?, status = ? WHERE id = ?",
build.BatchID, build.Status, build.ID,
"UPDATE build SET batch_id = ?, path_id = ?, attempt = ?, status = ? WHERE id = ?",
build.BatchID, build.PathID, build.Attempt, build.Status, build.ID,
)
if err != nil {
return fmt.Errorf("failed to update build entity id=%q: %w", build.ID, err)
Expand Down
28 changes: 16 additions & 12 deletions submitqueue/extension/storage/mysql/build_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func TestBuildStore_Get(t *testing.T) {
want := entity.Build{
ID: "bk-1001",
BatchID: "monorepo/batch/1",
PathID: "path-1",
Attempt: 1,
Status: entity.BuildStatusRunning,
}

Expand All @@ -59,9 +61,9 @@ func TestBuildStore_Get(t *testing.T) {
name: "found",
id: want.ID,
setup: func(mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{"id", "batch_id", "status"}).
AddRow(want.ID, want.BatchID, string(want.Status))
mock.ExpectQuery("SELECT id, batch_id, status").
rows := sqlmock.NewRows([]string{"id", "batch_id", "path_id", "attempt", "status"}).
AddRow(want.ID, want.BatchID, want.PathID, want.Attempt, string(want.Status))
mock.ExpectQuery("SELECT id, batch_id, path_id, attempt, status").
WithArgs(want.ID).
WillReturnRows(rows)
},
Expand All @@ -71,7 +73,7 @@ func TestBuildStore_Get(t *testing.T) {
name: "not found",
id: "missing",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectQuery("SELECT id, batch_id, status").
mock.ExpectQuery("SELECT id, batch_id, path_id, attempt, status").
WithArgs("missing").
WillReturnError(sql.ErrNoRows)
},
Expand All @@ -82,7 +84,7 @@ func TestBuildStore_Get(t *testing.T) {
name: "query error",
id: "bad",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectQuery("SELECT id, batch_id, status").
mock.ExpectQuery("SELECT id, batch_id, path_id, attempt, status").
WithArgs("bad").
WillReturnError(fmt.Errorf("connection reset"))
},
Expand Down Expand Up @@ -116,6 +118,8 @@ func TestBuildStore_Create(t *testing.T) {
build := entity.Build{
ID: "bk-1001",
BatchID: "monorepo/batch/1",
PathID: "path-1",
Attempt: 1,
Status: entity.BuildStatusAccepted,
}

Expand All @@ -129,15 +133,15 @@ func TestBuildStore_Create(t *testing.T) {
name: "success",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("INSERT INTO build").
WithArgs(build.ID, build.BatchID, build.Status).
WithArgs(build.ID, build.BatchID, build.PathID, build.Attempt, build.Status).
WillReturnResult(sqlmock.NewResult(0, 1))
},
},
{
name: "duplicate id returns ErrAlreadyExists",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("INSERT INTO build").
WithArgs(build.ID, build.BatchID, build.Status).
WithArgs(build.ID, build.BatchID, build.PathID, build.Attempt, build.Status).
WillReturnError(&mysql.MySQLError{Number: mysqlErrDuplicateEntry})
},
wantErr: true,
Expand All @@ -147,7 +151,7 @@ func TestBuildStore_Create(t *testing.T) {
name: "other exec error",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("INSERT INTO build").
WithArgs(build.ID, build.BatchID, build.Status).
WithArgs(build.ID, build.BatchID, build.PathID, build.Attempt, build.Status).
WillReturnError(fmt.Errorf("connection reset"))
},
wantErr: true,
Expand Down Expand Up @@ -192,15 +196,15 @@ func TestBuildStore_Update(t *testing.T) {
name: "success",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("UPDATE build").
WithArgs(build.BatchID, build.Status, build.ID).
WithArgs(build.BatchID, build.PathID, build.Attempt, build.Status, build.ID).
WillReturnResult(sqlmock.NewResult(0, 1))
},
},
{
name: "not found",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("UPDATE build").
WithArgs(build.BatchID, build.Status, build.ID).
WithArgs(build.BatchID, build.PathID, build.Attempt, build.Status, build.ID).
WillReturnResult(sqlmock.NewResult(0, 0))
},
wantErr: true,
Expand All @@ -210,7 +214,7 @@ func TestBuildStore_Update(t *testing.T) {
name: "exec error",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("UPDATE build").
WithArgs(build.BatchID, build.Status, build.ID).
WithArgs(build.BatchID, build.PathID, build.Attempt, build.Status, build.ID).
WillReturnError(fmt.Errorf("connection reset"))
},
wantErr: true,
Expand All @@ -219,7 +223,7 @@ func TestBuildStore_Update(t *testing.T) {
name: "rows affected error",
setup: func(mock sqlmock.Sqlmock) {
mock.ExpectExec("UPDATE build").
WithArgs(build.BatchID, build.Status, build.ID).
WithArgs(build.BatchID, build.PathID, build.Attempt, build.Status, build.ID).
WillReturnResult(sqlmock.NewErrorResult(fmt.Errorf("driver error")))
},
wantErr: true,
Expand Down
Loading
Loading