Skip to content

Commit 24f2c85

Browse files
authored
refactor(pusher): push ordered batches, return per-batch outcomes (#222)
## Summary Change Pusher.Push to take ordered []entity.Batch instead of controller-pre-resolved []entity.Change, per the extension contract. The git pusher and the fake gain an injected changeset.Resolver and resolve each batch's changes themselves; the merge controller drops its private collectChanges walk and passes the single batch (the list designs for a future merge-train). This is the one extension whose output shape also changes: Result now groups outcomes per batch — Result{Batches []BatchOutcome}, where BatchOutcome{BatchID, Outcomes []ChangeOutcome} — so each landed batch stays correlatable, the way conflict.Conflict carries its BatchID. ChangeOutcome (per-change commit detail) is unchanged. No per-batch status: push atomicity stays all-or-nothing across the whole call. ## Test Plan ## Issues ## Stack 1. #221 1. @ #222 1. #223 1. #227
1 parent 4eba54b commit 24f2c85

11 files changed

Lines changed: 205 additions & 219 deletions

File tree

example/submitqueue/orchestrator/server/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,16 +775,17 @@ func newChangeProvider(logger *zap.Logger, scope tally.Scope) (changeprovider.Ch
775775
// "origin", PUSHER_TARGET default "main"). When PUSHER_CHECKOUT_PATH is unset it
776776
// returns the fake pusher (commits succeed unless a change URI carries a failure
777777
// marker, see pusher/fake), keeping the example runnable without a git checkout.
778-
func newPusher(logger *zap.Logger, scope tally.Scope) (pusher.Pusher, error) {
778+
func newPusher(logger *zap.Logger, scope tally.Scope, resolver changeset.Resolver) (pusher.Pusher, error) {
779779
checkout := os.Getenv("PUSHER_CHECKOUT_PATH")
780780
if checkout == "" {
781781
logger.Warn("PUSHER_CHECKOUT_PATH not set; using fake pusher (commits succeed unless URI-marked)")
782-
return pushfake.New(), nil
782+
return pushfake.New(resolver), nil
783783
}
784784
return gitpusher.NewPusher(gitpusher.Params{
785785
CheckoutPath: checkout,
786786
Remote: getEnv("PUSHER_REMOTE", "origin"),
787787
Target: getEnv("PUSHER_TARGET", "main"),
788+
Resolver: resolver,
788789
Logger: logger.Sugar(),
789790
MetricsScope: scope.SubScope("pusher"),
790791
}), nil
@@ -806,7 +807,7 @@ func newQueueRegistry(logger *zap.Logger, scope tally.Scope, resolver changeset.
806807
if err != nil {
807808
return queueRegistry{}, fmt.Errorf("failed to create change provider: %w", err)
808809
}
809-
psh, err := newPusher(logger, scope)
810+
psh, err := newPusher(logger, scope, resolver)
810811
if err != nil {
811812
return queueRegistry{}, fmt.Errorf("failed to create pusher: %w", err)
812813
}

submitqueue/extension/pusher/fake/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ go_library(
66
importpath = "github.com/uber/submitqueue/submitqueue/extension/pusher/fake",
77
visibility = ["//visibility:public"],
88
deps = [
9+
"//submitqueue/core/changeset",
910
"//submitqueue/core/fakemarker",
1011
"//submitqueue/entity",
1112
"//submitqueue/extension/pusher",
@@ -17,6 +18,7 @@ go_test(
1718
srcs = ["fake_test.go"],
1819
embed = [":fake"],
1920
deps = [
21+
"//submitqueue/core/changeset/fake",
2022
"//submitqueue/entity",
2123
"//submitqueue/extension/pusher",
2224
"@com_github_stretchr_testify//assert",

submitqueue/extension/pusher/fake/fake.go

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"fmt"
3333
"sync/atomic"
3434

35+
"github.com/uber/submitqueue/submitqueue/core/changeset"
3536
"github.com/uber/submitqueue/submitqueue/core/fakemarker"
3637
"github.com/uber/submitqueue/submitqueue/entity"
3738
"github.com/uber/submitqueue/submitqueue/extension/pusher"
@@ -45,35 +46,54 @@ const (
4546

4647
// fakePusher is a pusher.Pusher that reports every change as committed unless a
4748
// marker token in a change URI requests a failure. The atomic counter hands out
48-
// unique synthetic commit SHAs and makes the type safe for concurrent use.
49+
// unique synthetic commit SHAs and makes the type safe for concurrent use. It
50+
// resolves each batch's changes through the injected resolver.
4951
type fakePusher struct {
50-
counter atomic.Uint64
52+
resolver changeset.Resolver
53+
counter atomic.Uint64
5154
}
5255

5356
// New returns a pusher.Pusher that defaults to committing every change and
54-
// honors marker tokens embedded in change URIs.
55-
func New() pusher.Pusher {
56-
return &fakePusher{}
57+
// honors marker tokens embedded in change URIs. The resolver resolves each
58+
// batch's changes.
59+
func New(resolver changeset.Resolver) pusher.Pusher {
60+
return &fakePusher{resolver: resolver}
5761
}
5862

59-
// Push reports every change as committed with a synthetic commit SHA, unless a
60-
// recognized marker token in one of the changes requests a failure.
61-
func (p *fakePusher) Push(_ context.Context, changes []entity.Change) (pusher.Result, error) {
62-
switch fakemarker.TokenInChanges(changes) {
63+
// Push resolves each batch's changes and reports every change as committed with
64+
// a synthetic commit SHA, grouped per batch, unless a recognized marker token in
65+
// one of the changes requests a failure.
66+
func (p *fakePusher) Push(ctx context.Context, batches []entity.Batch) (pusher.Result, error) {
67+
perBatch := make([][]entity.Change, len(batches))
68+
var all []entity.Change
69+
for i, b := range batches {
70+
cs, err := p.resolver.ChangesForBatch(ctx, b)
71+
if err != nil {
72+
return pusher.Result{}, fmt.Errorf("fake: resolve batch %s: %w", b.ID, err)
73+
}
74+
perBatch[i] = cs
75+
all = append(all, cs...)
76+
}
77+
78+
switch fakemarker.TokenInChanges(all) {
6379
case tokenConflict:
6480
return pusher.Result{}, pusher.ErrConflict
6581
case tokenError:
6682
return pusher.Result{}, fmt.Errorf("fake: marked push error")
6783
}
6884

69-
outcomes := make([]pusher.ChangeOutcome, 0, len(changes))
70-
for _, change := range changes {
71-
sha := fmt.Sprintf("fake-%d", p.counter.Add(1))
72-
outcomes = append(outcomes, pusher.ChangeOutcome{
73-
Change: change,
74-
Status: pusher.OutcomeStatusCommitted,
75-
CommitSHAs: []string{sha},
76-
})
85+
result := make([]pusher.BatchOutcome, len(batches))
86+
for i, b := range batches {
87+
outcomes := make([]pusher.ChangeOutcome, 0, len(perBatch[i]))
88+
for _, change := range perBatch[i] {
89+
sha := fmt.Sprintf("fake-%d", p.counter.Add(1))
90+
outcomes = append(outcomes, pusher.ChangeOutcome{
91+
Change: change,
92+
Status: pusher.OutcomeStatusCommitted,
93+
CommitSHAs: []string{sha},
94+
})
95+
}
96+
result[i] = pusher.BatchOutcome{BatchID: b.ID, Outcomes: outcomes}
7797
}
78-
return pusher.Result{Outcomes: outcomes}, nil
98+
return pusher.Result{Batches: result}, nil
7999
}

submitqueue/extension/pusher/fake/fake_test.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,29 @@ import (
2121

2222
"github.com/stretchr/testify/assert"
2323
"github.com/stretchr/testify/require"
24+
changesetfake "github.com/uber/submitqueue/submitqueue/core/changeset/fake"
2425
"github.com/uber/submitqueue/submitqueue/entity"
2526
"github.com/uber/submitqueue/submitqueue/extension/pusher"
2627
)
2728

2829
func TestNew_ImplementsInterface(t *testing.T) {
29-
var _ pusher.Pusher = New()
30+
var _ pusher.Pusher = New(changesetfake.New())
3031
}
3132

3233
func TestPusher_Push_Committed(t *testing.T) {
33-
p := New()
3434
changes := []entity.Change{
3535
{URIs: []string{"github://owner/repo/pull/1/abc"}},
3636
{URIs: []string{"github://owner/repo/pull/2/def"}},
3737
}
38+
p := New(changesetfake.New().Set("b", changes...))
3839

39-
res, err := p.Push(context.Background(), changes)
40+
res, err := p.Push(context.Background(), []entity.Batch{{ID: "b"}})
4041
require.NoError(t, err)
41-
require.Len(t, res.Outcomes, len(changes))
42+
require.Len(t, res.Batches, 1)
43+
require.Len(t, res.Batches[0].Outcomes, len(changes))
4244

4345
seen := map[string]bool{}
44-
for i, out := range res.Outcomes {
46+
for i, out := range res.Batches[0].Outcomes {
4547
assert.Equal(t, changes[i], out.Change)
4648
assert.Equal(t, pusher.OutcomeStatusCommitted, out.Status)
4749
require.Len(t, out.CommitSHAs, 1)
@@ -51,19 +53,15 @@ func TestPusher_Push_Committed(t *testing.T) {
5153
}
5254

5355
func TestPusher_Push_ConflictMarker(t *testing.T) {
54-
p := New()
55-
_, err := p.Push(context.Background(), []entity.Change{
56-
{URIs: []string{"github://owner/repo/pull/1/abc?sq-fake=conflict"}},
57-
})
56+
p := New(changesetfake.New().Set("b", entity.Change{URIs: []string{"github://owner/repo/pull/1/abc?sq-fake=conflict"}}))
57+
_, err := p.Push(context.Background(), []entity.Batch{{ID: "b"}})
5858
assert.True(t, errors.Is(err, pusher.ErrConflict))
5959
}
6060

6161
func TestPusher_Push_ErrorMarker(t *testing.T) {
62-
p := New()
63-
res, err := p.Push(context.Background(), []entity.Change{
64-
{URIs: []string{"github://owner/repo/pull/1/abc?sq-fake=push-error"}},
65-
})
62+
p := New(changesetfake.New().Set("b", entity.Change{URIs: []string{"github://owner/repo/pull/1/abc?sq-fake=push-error"}}))
63+
res, err := p.Push(context.Background(), []entity.Batch{{ID: "b"}})
6664
require.Error(t, err)
6765
// Atomicity: on error no outcomes are reported.
68-
assert.Empty(t, res.Outcomes)
66+
assert.Empty(t, res.Batches)
6967
}

submitqueue/extension/pusher/git/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_library(
77
visibility = ["//visibility:public"],
88
deps = [
99
"//core/metrics",
10+
"//submitqueue/core/changeset",
1011
"//submitqueue/entity",
1112
"//submitqueue/entity/github",
1213
"//submitqueue/extension/pusher",
@@ -20,6 +21,7 @@ go_test(
2021
srcs = ["git_pusher_test.go"],
2122
embed = [":git"],
2223
deps = [
24+
"//submitqueue/core/changeset/fake",
2325
"//submitqueue/entity",
2426
"//submitqueue/extension/pusher",
2527
"@com_github_stretchr_testify//assert",

submitqueue/extension/pusher/git/git_pusher.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import (
6262
"go.uber.org/zap"
6363

6464
coremetrics "github.com/uber/submitqueue/core/metrics"
65+
"github.com/uber/submitqueue/submitqueue/core/changeset"
6566
"github.com/uber/submitqueue/submitqueue/entity"
6667
entitygithub "github.com/uber/submitqueue/submitqueue/entity/github"
6768
"github.com/uber/submitqueue/submitqueue/extension/pusher"
@@ -82,6 +83,8 @@ type Params struct {
8283
Remote string
8384
// Target is the destination branch ref on the remote (e.g. "main").
8485
Target string
86+
// Resolver resolves each batch's changes.
87+
Resolver changeset.Resolver
8588
// Logger is the structured logger.
8689
Logger *zap.SugaredLogger
8790
// MetricsScope is the metrics scope for instrumentation.
@@ -98,6 +101,7 @@ type gitPusher struct {
98101
checkoutPath string
99102
remote string
100103
target string
104+
resolver changeset.Resolver
101105
logger *zap.SugaredLogger
102106
metricsScope tally.Scope
103107
maxPushAttempts int
@@ -121,17 +125,31 @@ func NewPusher(params Params) pusher.Pusher {
121125
checkoutPath: params.CheckoutPath,
122126
remote: params.Remote,
123127
target: params.Target,
128+
resolver: params.Resolver,
124129
logger: params.Logger.Named("git_pusher"),
125130
metricsScope: params.MetricsScope.SubScope("git_pusher"),
126131
maxPushAttempts: maxAttempts,
127132
}
128133
}
129134

130135
// Push fulfils the pusher.Pusher contract.
131-
func (p *gitPusher) Push(ctx context.Context, changes []entity.Change) (ret pusher.Result, retErr error) {
136+
func (p *gitPusher) Push(ctx context.Context, batches []entity.Batch) (ret pusher.Result, retErr error) {
132137
op := coremetrics.Begin(p.metricsScope, "push")
133138
defer func() { op.Complete(retErr) }()
134139

140+
// Resolve each batch's changes, keeping per-batch counts so the flat
141+
// outcomes can be regrouped per batch on success.
142+
perBatch := make([][]entity.Change, len(batches))
143+
var changes []entity.Change
144+
for i, b := range batches {
145+
cs, err := p.resolver.ChangesForBatch(ctx, b)
146+
if err != nil {
147+
return pusher.Result{}, fmt.Errorf("resolve batch %s: %w", b.ID, err)
148+
}
149+
perBatch[i] = cs
150+
changes = append(changes, cs...)
151+
}
152+
135153
p.mu.Lock()
136154
defer p.mu.Unlock()
137155

@@ -154,7 +172,7 @@ func (p *gitPusher) Push(ctx context.Context, changes []entity.Change) (ret push
154172
"target", p.target,
155173
"outcomes", outcomes,
156174
)
157-
return pusher.Result{Outcomes: outcomes}, nil
175+
return pusher.Result{Batches: groupByBatch(batches, perBatch, outcomes)}, nil
158176
}
159177

160178
// Was the failure caused by the remote tip moving under us between
@@ -190,6 +208,19 @@ func (p *gitPusher) Push(ctx context.Context, changes []entity.Change) (ret push
190208
return pusher.Result{}, fmt.Errorf("exceeded %d push attempts due to remote contention: %w", p.maxPushAttempts, lastErr)
191209
}
192210

211+
// groupByBatch splits the flat, apply-ordered outcomes back into one
212+
// BatchOutcome per input batch, using each batch's resolved change count.
213+
func groupByBatch(batches []entity.Batch, perBatch [][]entity.Change, outcomes []pusher.ChangeOutcome) []pusher.BatchOutcome {
214+
result := make([]pusher.BatchOutcome, len(batches))
215+
pos := 0
216+
for i, b := range batches {
217+
n := len(perBatch[i])
218+
result[i] = pusher.BatchOutcome{BatchID: b.ID, Outcomes: outcomes[pos : pos+n]}
219+
pos += n
220+
}
221+
return result
222+
}
223+
193224
// tryPush runs one full reset+cherry-pick+push cycle. The returned baseSHA
194225
// is the SHA the cycle was based on (set as soon as resetToRemote completes)
195226
// so the caller can distinguish concurrent-push contention from other push

0 commit comments

Comments
 (0)