Skip to content
Draft
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
3 changes: 3 additions & 0 deletions submitqueue/entity/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ go_library(
"cancel_request.go",
"change_provider.go",
"change_record.go",
"conflict.go",
"land_request.go",
"merge_result.go",
"push_result.go",
"queue_config.go",
"request.go",
"request_log.go",
Expand Down
47 changes: 47 additions & 0 deletions submitqueue/entity/conflict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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

// ConflictType classifies why two batches are considered to conflict.
// New values may be added as more sophisticated analyzers are introduced.
type ConflictType string

const (
// ConflictTypeUnknown is the unreachable zero value, set by default when
// the structure is initialized. It should never be seen in the system.
ConflictTypeUnknown ConflictType = ""
// ConflictTypeConservative means the analyzer treated the batches as
// conflicting because it could not prove otherwise, without identifying a
// specific reason. Used by conservative analyzers that serialize
// everything by default.
ConflictTypeConservative ConflictType = "conservative"
// ConflictTypeTargetOverlap means the two batches modify one or more of
// the same build targets and may therefore interfere with each other.
ConflictTypeTargetOverlap ConflictType = "target_overlap"
)

// Conflict reports a single conflict between an analyzed batch and one of the
// in-flight batches. It lives in the entity package because a conflict is a
// domain fact about the dependency graph that the orchestrator may persist or
// surface, independent of the analyzer that produces it.
type Conflict struct {
// BatchID is the ID of the in-flight batch that conflicts with the
// analyzed batch.
BatchID string
// Type classifies the conflict. A single (analyzed, in-flight) pair may
// be reported with multiple Conflict entries when different conflict
// types apply.
Type ConflictType
}
27 changes: 27 additions & 0 deletions submitqueue/entity/merge_result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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

// MergeResult holds the outcome of a mergeability check. It lives in the entity
// package because the verdict (and the reason for a rejection) is a domain fact
// the orchestrator may persist or surface, independent of the merge checker that
// produces it.
type MergeResult struct {
// Mergeable is true if the request's changes are expected to merge cleanly.
Mergeable bool
// Reason is a human-readable explanation when Mergeable is false.
// Empty when Mergeable is true.
Reason string
}
68 changes: 68 additions & 0 deletions submitqueue/entity/push_result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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

// OutcomeStatus describes what happened to a single Change during a push.
type OutcomeStatus string

const (
// OutcomeStatusUnknown is the unreachable zero value, set by default
// when the structure is initialized. It should never be seen in the system.
OutcomeStatusUnknown OutcomeStatus = ""
// OutcomeStatusCommitted means the change produced one or more commits
// on the target branch. CommitSHAs lists those commits in apply order.
OutcomeStatusCommitted OutcomeStatus = "committed"
// OutcomeStatusAlreadyExisted means the change produced no commits
// because every part of it is already present in the target branch
// (e.g. it previously landed via another path, or a prior change in
// the same push subsumed it). CommitSHAs is empty for this status.
// In git terms this is what a `cherry-pick` surfaces as "rebased out".
OutcomeStatusAlreadyExisted OutcomeStatus = "already_existed"
)

// ChangeOutcome describes what happened to a single Change inside a push. It
// lives in the entity package because the commits a change produced are a domain
// fact the orchestrator may persist or surface (e.g. "this PR landed as commit
// abc"), independent of the pusher that produces it.
type ChangeOutcome struct {
// Change is the input change this outcome corresponds to.
Change Change
// Status describes whether the change produced commits or was already
// present on the target branch.
Status OutcomeStatus
// CommitSHAs lists the commits this change produced on the target
// branch, in apply order. A single Change may produce multiple commits
// (e.g. a stack of PRs). Empty when Status is OutcomeStatusAlreadyExisted.
CommitSHAs []string
}

// BatchOutcome groups the per-change outcomes for a single pushed batch, so a
// merge-train push (several batches in one call) stays correlatable back to the
// batch each change belonged to. There is no per-batch status: a push is
// all-or-nothing across the whole call, so a per-batch pass/fail would be
// uniformly redundant.
type BatchOutcome struct {
// BatchID is the input batch this outcome corresponds to.
BatchID string
// Outcomes is one entry per change in the batch, in apply order.
Outcomes []ChangeOutcome
}

// PushResult is the outcome of a successful push.
type PushResult struct {
// Batches is one entry per pushed batch, in the same order as the batches
// passed to the push. The slice length equals the input length.
Batches []BatchOutcome
}
1 change: 0 additions & 1 deletion submitqueue/extension/conflict/all/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ go_test(
embed = [":all"],
deps = [
"//submitqueue/entity",
"//submitqueue/extension/conflict",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
Expand Down
8 changes: 4 additions & 4 deletions submitqueue/extension/conflict/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ func New() conflict.Analyzer {

// Analyze returns one ConflictTypeConservative Conflict per in-flight batch,
// preserving the input order. Returns an empty slice when inFlight is empty.
func (analyzer) Analyze(_ context.Context, _ entity.Batch, inFlight []entity.Batch) ([]conflict.Conflict, error) {
func (analyzer) Analyze(_ context.Context, _ entity.Batch, inFlight []entity.Batch) ([]entity.Conflict, error) {
if len(inFlight) == 0 {
return nil, nil
}
conflicts := make([]conflict.Conflict, len(inFlight))
conflicts := make([]entity.Conflict, len(inFlight))
for i, b := range inFlight {
conflicts[i] = conflict.Conflict{
conflicts[i] = entity.Conflict{
BatchID: b.ID,
Type: conflict.ConflictTypeConservative,
Type: entity.ConflictTypeConservative,
}
}
return conflicts, nil
Expand Down
11 changes: 5 additions & 6 deletions submitqueue/extension/conflict/all/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/submitqueue/submitqueue/entity"
"github.com/uber/submitqueue/submitqueue/extension/conflict"
)

func TestAnalyze(t *testing.T) {
Expand All @@ -30,7 +29,7 @@ func TestAnalyze(t *testing.T) {
tests := []struct {
name string
inFlight []entity.Batch
want []conflict.Conflict
want []entity.Conflict
}{
{
name: "no in-flight batches yields no conflicts",
Expand All @@ -49,10 +48,10 @@ func TestAnalyze(t *testing.T) {
{ID: "queueA/batch/2"},
{ID: "queueA/batch/3"},
},
want: []conflict.Conflict{
{BatchID: "queueA/batch/1", Type: conflict.ConflictTypeConservative},
{BatchID: "queueA/batch/2", Type: conflict.ConflictTypeConservative},
{BatchID: "queueA/batch/3", Type: conflict.ConflictTypeConservative},
want: []entity.Conflict{
{BatchID: "queueA/batch/1", Type: entity.ConflictTypeConservative},
{BatchID: "queueA/batch/2", Type: entity.ConflictTypeConservative},
{BatchID: "queueA/batch/3", Type: entity.ConflictTypeConservative},
},
},
}
Expand Down
32 changes: 1 addition & 31 deletions submitqueue/extension/conflict/conflict.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,6 @@ import (
"github.com/uber/submitqueue/submitqueue/entity"
)

// ConflictType classifies why two batches are considered to conflict.
// New values may be added as more sophisticated analyzers are introduced.
type ConflictType string

const (
// ConflictTypeUnknown is the unreachable zero value, set by default when
// the structure is initialized. It should never be seen in the system.
ConflictTypeUnknown ConflictType = ""
// ConflictTypeConservative means the analyzer treated the batches as
// conflicting because it could not prove otherwise, without identifying a
// specific reason. Used by conservative analyzers that serialize
// everything by default.
ConflictTypeConservative ConflictType = "conservative"
// ConflictTypeTargetOverlap means the two batches modify one or more of
// the same build targets and may therefore interfere with each other.
ConflictTypeTargetOverlap ConflictType = "target_overlap"
)

// Conflict reports a single conflict between the analyzed batch and one of
// the in-flight batches.
type Conflict struct {
// BatchID is the ID of the in-flight batch that conflicts with the
// analyzed batch.
BatchID string
// Type classifies the conflict. A single (analyzed, in-flight) pair may
// be reported with multiple Conflict entries when different conflict
// types apply.
Type ConflictType
}

// Analyzer detects conflicts between a candidate batch and the batches
// already in flight, so the speculation layer can decide which batches can
// safely advance in parallel.
Expand All @@ -64,7 +34,7 @@ type Analyzer interface {
// should be filtered out before calling. A non-nil error indicates the
// analysis itself failed (infrastructure issue) and should be treated as
// retryable by the caller.
Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]Conflict, error)
Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]entity.Conflict, error)
}

// Config carries the per-queue identity handed to a Factory. The system knows
Expand Down
2 changes: 1 addition & 1 deletion submitqueue/extension/conflict/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func New(delegate conflict.Analyzer, failOn FailOn) conflict.Analyzer {

// Analyze returns an error when failOn reports true; otherwise it delegates to
// the wrapped analyzer.
func (a analyzerFake) Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]conflict.Conflict, error) {
func (a analyzerFake) Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]entity.Conflict, error) {
if a.failOn != nil && a.failOn(batch, inFlight) {
return nil, fmt.Errorf("fake: injected analyze error for batch %q", batch.ID)
}
Expand Down
1 change: 0 additions & 1 deletion submitqueue/extension/conflict/fileoverlap/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ go_test(
deps = [
"//submitqueue/core/changeset/fake",
"//submitqueue/entity",
"//submitqueue/extension/conflict",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
Expand Down
10 changes: 5 additions & 5 deletions submitqueue/extension/conflict/fileoverlap/fileoverlap.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// only batch identity and resolves each batch's changed files itself through an
// injected changeset resolver, rather than depending on the controller to
// pre-compute them. A shared file is the concrete notion of target overlap, so
// it reports conflict.ConflictTypeTargetOverlap.
// it reports entity.ConflictTypeTargetOverlap.
package fileoverlap

import (
Expand Down Expand Up @@ -46,7 +46,7 @@ func New(resolver changeset.Resolver) conflict.Analyzer {
// Analyze returns one ConflictTypeTargetOverlap Conflict per in-flight batch
// that shares a changed file with batch, preserving the in-flight order. A batch
// that changes no files conflicts with nothing.
func (a analyzer) Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]conflict.Conflict, error) {
func (a analyzer) Analyze(ctx context.Context, batch entity.Batch, inFlight []entity.Batch) ([]entity.Conflict, error) {
if len(inFlight) == 0 {
return nil, nil
}
Expand All @@ -59,16 +59,16 @@ func (a analyzer) Analyze(ctx context.Context, batch entity.Batch, inFlight []en
return nil, nil
}

var conflicts []conflict.Conflict
var conflicts []entity.Conflict
for _, other := range inFlight {
files, err := a.files(ctx, other)
if err != nil {
return nil, fmt.Errorf("failed to resolve files for batch %s: %w", other.ID, err)
}
if intersects(candidate, files) {
conflicts = append(conflicts, conflict.Conflict{
conflicts = append(conflicts, entity.Conflict{
BatchID: other.ID,
Type: conflict.ConflictTypeTargetOverlap,
Type: entity.ConflictTypeTargetOverlap,
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (

changesetfake "github.com/uber/submitqueue/submitqueue/core/changeset/fake"
"github.com/uber/submitqueue/submitqueue/entity"
"github.com/uber/submitqueue/submitqueue/extension/conflict"
)

// detailed builds a BatchChanges whose single change touches the given files.
Expand Down Expand Up @@ -101,7 +100,7 @@ func TestAnalyze(t *testing.T) {

var ids []string
for _, c := range got {
assert.Equal(t, conflict.ConflictTypeTargetOverlap, c.Type)
assert.Equal(t, entity.ConflictTypeTargetOverlap, c.Type)
ids = append(ids, c.BatchID)
}
assert.Equal(t, tt.wantBatches, ids)
Expand Down
4 changes: 2 additions & 2 deletions submitqueue/extension/conflict/mock/conflict_mock.go

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

2 changes: 1 addition & 1 deletion submitqueue/extension/conflict/none/none.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ func New() conflict.Analyzer {
}

// Analyze always returns a nil conflict slice, regardless of inputs.
func (analyzer) Analyze(_ context.Context, _ entity.Batch, _ []entity.Batch) ([]conflict.Conflict, error) {
func (analyzer) Analyze(_ context.Context, _ entity.Batch, _ []entity.Batch) ([]entity.Conflict, error) {
return nil, nil
}
8 changes: 4 additions & 4 deletions submitqueue/extension/mergechecker/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ func New() mergechecker.MergeChecker {

// Check reports the change as mergeable unless a recognized marker token is
// present in one of the request's change URIs.
func (checker) Check(_ context.Context, request entity.Request) (mergechecker.Result, error) {
func (checker) Check(_ context.Context, request entity.Request) (entity.MergeResult, error) {
switch fakemarker.Token(request.Change.URIs) {
case tokenUnmergeable:
return mergechecker.Result{Mergeable: false, Reason: "fake: marked unmergeable"}, nil
return entity.MergeResult{Mergeable: false, Reason: "fake: marked unmergeable"}, nil
case tokenError:
return mergechecker.Result{}, fmt.Errorf("fake: marked merge-check error")
return entity.MergeResult{}, fmt.Errorf("fake: marked merge-check error")
default:
return mergechecker.Result{Mergeable: true}, nil
return entity.MergeResult{Mergeable: true}, nil
}
}
2 changes: 1 addition & 1 deletion submitqueue/extension/mergechecker/github/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func NewMergeChecker(params Params) mergechecker.MergeChecker {
}

// Check assesses whether a request's change can merge cleanly using the GitHub GraphQL API.
func (c *mergeChecker) Check(ctx context.Context, request entity.Request) (result mergechecker.Result, retErr error) {
func (c *mergeChecker) Check(ctx context.Context, request entity.Request) (result entity.MergeResult, retErr error) {
const opName = "check"

op := metrics.Begin(c.metricsScope, opName)
Expand Down
11 changes: 1 addition & 10 deletions submitqueue/extension/mergechecker/mergechecker.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,7 @@ type MergeChecker interface {
// whether the request's changes can be merged. It is handed the request
// identity and reads request.Change itself. A positive result does not
// guarantee that the changes will apply cleanly at merge time.
Check(ctx context.Context, request entity.Request) (Result, error)
}

// Result holds the outcome of a mergeability check.
type Result struct {
// Mergeable is true if the request's changes are expected to merge cleanly.
Mergeable bool
// Reason is a human-readable explanation when Mergeable is false.
// Empty when Mergeable is true.
Reason string
Check(ctx context.Context, request entity.Request) (entity.MergeResult, error)
}

// Config carries the per-queue identity handed to a Factory. The system knows
Expand Down
Loading