Skip to content

Commit 24fac27

Browse files
kevinlnewclaude
andcommitted
feat(pushqueue): add land and ping controllers with tests
Land controller orchestrates enqueue → wait → push → finalize flow with error classification (conflict=user, stale head=retryable). Ping controller uses SugaredLogger. Regenerates mocks with go.uber.org/mock and fixes proto BUILD disambiguation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4d2c36b commit 24fac27

6 files changed

Lines changed: 718 additions & 0 deletions

File tree

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ load("@gazelle//:def.bzl", "gazelle")
1111
# gazelle:resolve go github.com/uber/submitqueue/submitqueue/orchestrator/protopb //submitqueue/orchestrator/protopb
1212
# gazelle:resolve go github.com/uber/submitqueue/stovepipe/gateway/protopb //stovepipe/gateway/protopb
1313
# gazelle:resolve go github.com/uber/submitqueue/stovepipe/orchestrator/protopb //stovepipe/orchestrator/protopb
14+
# gazelle:resolve go github.com/uber/submitqueue/pushqueue/gateway/protopb //pushqueue/gateway/protopb
1415

1516
# Export marker files for test data dependencies (used by FindRepoRoot in tests)
1617
exports_files(
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "controller",
5+
srcs = [
6+
"land.go",
7+
"ping.go",
8+
],
9+
importpath = "github.com/uber/submitqueue/pushqueue/gateway/controller",
10+
visibility = ["//visibility:public"],
11+
deps = [
12+
"//core/errs",
13+
"//core/metrics",
14+
"//pushqueue/entity",
15+
"//pushqueue/extension/landqueue",
16+
"//pushqueue/extension/vcs",
17+
"//pushqueue/gateway/protopb",
18+
"@com_github_uber_go_tally_v4//:tally",
19+
"@org_uber_go_zap//:zap",
20+
],
21+
)
22+
23+
go_test(
24+
name = "controller_test",
25+
srcs = [
26+
"land_test.go",
27+
"ping_test.go",
28+
],
29+
embed = [":controller"],
30+
deps = [
31+
"//core/errs",
32+
"//pushqueue/entity",
33+
"//pushqueue/extension/landqueue/mock",
34+
"//pushqueue/extension/vcs",
35+
"//pushqueue/extension/vcs/mock",
36+
"//pushqueue/gateway/protopb",
37+
"@com_github_stretchr_testify//assert",
38+
"@com_github_stretchr_testify//require",
39+
"@com_github_uber_go_tally_v4//:tally",
40+
"@org_uber_go_mock//gomock",
41+
"@org_uber_go_zap//:zap",
42+
"@org_uber_go_zap//zaptest",
43+
],
44+
)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controller
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
22+
"github.com/uber-go/tally/v4"
23+
"github.com/uber/submitqueue/core/errs"
24+
"github.com/uber/submitqueue/core/metrics"
25+
"github.com/uber/submitqueue/pushqueue/entity"
26+
"github.com/uber/submitqueue/pushqueue/extension/landqueue"
27+
"github.com/uber/submitqueue/pushqueue/extension/vcs"
28+
pb "github.com/uber/submitqueue/pushqueue/gateway/protopb"
29+
"go.uber.org/zap"
30+
)
31+
32+
// LandController handles land and mergeability business logic for the
33+
// pushqueue gateway.
34+
type LandController struct {
35+
logger *zap.SugaredLogger
36+
metricsScope tally.Scope
37+
vcs vcs.VCS
38+
queue landqueue.Queue
39+
}
40+
41+
// NewLandController creates a new instance of the pushqueue land controller.
42+
func NewLandController(logger *zap.SugaredLogger, scope tally.Scope, v vcs.VCS, q landqueue.Queue) *LandController {
43+
return &LandController{
44+
logger: logger.Named("land_controller"),
45+
metricsScope: scope.SubScope("land_controller"),
46+
vcs: v,
47+
queue: q,
48+
}
49+
}
50+
51+
// Land enqueues items for landing, waits for head-of-queue, pushes via VCS,
52+
// and finalizes. Preparation is handled by the queue's Preparer.
53+
func (c *LandController) Land(ctx context.Context, req *pb.LandRequest) (resp *pb.LandResponse, retErr error) {
54+
const opName = "land"
55+
56+
op := metrics.Begin(c.metricsScope, opName)
57+
defer func() { op.Complete(retErr) }()
58+
59+
target := entity.QueueTarget{
60+
Name: req.Queue.GetName(),
61+
Address: req.Queue.GetAddress(),
62+
Target: req.Queue.GetTarget(),
63+
}
64+
65+
items := make([]entity.LandItem, 0, len(req.Items))
66+
for _, item := range req.Items {
67+
items = append(items, entity.LandItem{
68+
URIs: item.GetUris(),
69+
Strategy: resolveStrategy(item.GetStrategy()),
70+
})
71+
}
72+
73+
if err := c.queue.Enqueue(ctx, target, items); err != nil {
74+
return nil, fmt.Errorf("enqueue: %w", err)
75+
}
76+
defer c.queue.Dequeue(ctx, target)
77+
78+
if err := c.queue.Wait(ctx, target); err != nil {
79+
return nil, fmt.Errorf("wait: %w", err)
80+
}
81+
82+
result, pushErr := c.vcs.Push(ctx, target, items)
83+
switch {
84+
case pushErr == nil:
85+
case errors.Is(pushErr, vcs.ErrConflict):
86+
return nil, errs.NewUserError(fmt.Errorf("conflict: %w", pushErr))
87+
case errors.Is(pushErr, vcs.ErrStaleHead):
88+
return nil, errs.NewRetryableError(fmt.Errorf("stale head: %w", pushErr))
89+
default:
90+
return nil, fmt.Errorf("push: %w", pushErr)
91+
}
92+
93+
resp = &pb.LandResponse{
94+
Success: true,
95+
Outcomes: make([]*pb.ItemOutcome, 0, len(result.Outcomes)),
96+
}
97+
for _, o := range result.Outcomes {
98+
resp.Outcomes = append(resp.Outcomes, &pb.ItemOutcome{
99+
Status: outcomeStatusToProto(o.Status),
100+
RevisionIds: o.RevisionIDs,
101+
})
102+
}
103+
104+
if err := c.vcs.Finalize(ctx, target, items); err != nil {
105+
resp.FinalizeError = err.Error()
106+
}
107+
108+
return resp, nil
109+
}
110+
111+
// CheckMergeability checks whether items can be landed on the target.
112+
func (c *LandController) CheckMergeability(ctx context.Context, req *pb.CheckMergeabilityRequest) (resp *pb.CheckMergeabilityResponse, retErr error) {
113+
const opName = "check_mergeability"
114+
115+
op := metrics.Begin(c.metricsScope, opName)
116+
defer func() { op.Complete(retErr) }()
117+
118+
target := entity.QueueTarget{
119+
Name: req.Queue.GetName(),
120+
Address: req.Queue.GetAddress(),
121+
Target: req.Queue.GetTarget(),
122+
}
123+
124+
items := make([]entity.LandItem, 0, len(req.Items))
125+
for _, item := range req.Items {
126+
items = append(items, entity.LandItem{
127+
URIs: item.GetUris(),
128+
Strategy: resolveStrategy(item.GetStrategy()),
129+
})
130+
}
131+
132+
results, err := c.vcs.CheckMergeability(ctx, target, items)
133+
if err != nil {
134+
return nil, fmt.Errorf("check mergeability: %w", err)
135+
}
136+
137+
allMergeable := true
138+
protoResults := make([]*pb.MergeabilityItemResult, 0, len(results))
139+
for _, r := range results {
140+
if !r.Mergeable {
141+
allMergeable = false
142+
}
143+
protoResults = append(protoResults, &pb.MergeabilityItemResult{
144+
Mergeable: r.Mergeable,
145+
Reason: r.Reason,
146+
})
147+
}
148+
149+
return &pb.CheckMergeabilityResponse{
150+
Mergeable: allMergeable,
151+
Results: protoResults,
152+
}, nil
153+
}
154+
155+
func resolveStrategy(s pb.Strategy) entity.LandStrategy {
156+
switch s {
157+
case pb.Strategy_STRATEGY_REBASE:
158+
return entity.LandStrategyRebase
159+
case pb.Strategy_STRATEGY_SQUASH_REBASE:
160+
return entity.LandStrategySquashRebase
161+
case pb.Strategy_STRATEGY_MERGE:
162+
return entity.LandStrategyMerge
163+
default:
164+
return entity.LandStrategyUnknown
165+
}
166+
}
167+
168+
func outcomeStatusToProto(s vcs.OutcomeStatus) pb.OutcomeStatus {
169+
switch s {
170+
case vcs.OutcomeStatusCommitted:
171+
return pb.OutcomeStatus_OUTCOME_STATUS_COMMITTED
172+
case vcs.OutcomeStatusAlreadyExisted:
173+
return pb.OutcomeStatus_OUTCOME_STATUS_ALREADY_EXISTED
174+
default:
175+
return pb.OutcomeStatus_OUTCOME_STATUS_UNSPECIFIED
176+
}
177+
}

0 commit comments

Comments
 (0)