Skip to content

Commit 2993629

Browse files
authored
feat(conflict): Add Tango request and signature intersection (#884)
# Why? The Tango-backed conflict analyzer needs three building blocks that don't depend on how it is wired: the interfaces for the two things a deployment supplies, the request it sends to Tango, and the check that decides whether two signatures conflict. This PR adds them separately so the interface shapes can be reviewed before the analyzer uses them. ## What? Everything is added under `submitqueue/extension/conflict/tango`: - Adds `ChangedTargetsRequest`: the remote, the base revision, every change URI in batch order, and the merge strategy. - Adds `ChangedTargetsClient`, which returns the affected targets of a request as label and distance pairs. A deployment supplies an adapter that owns the Tango client and its stream, collects the response and resolves each target's label. Any failure to produce a complete answer is an error, and an empty result is valid. - Adds `Base` and `BaseResolver`: how the analyzer learns the newest revision of a queue's target branch. A deployment supplies the implementation. - Adds `changedTargetsRequestFor`, which flattens the URIs of a batch's changes, in order, into a request. - Adds `sharesTarget`, which reports whether two signatures have a target ID in common. Both must be sorted by ID, so the check is one pass over each, and it stops at the first shared ID. - Adds gomock mocks for the two interfaces, generated in the same package. - Nothing calls this code yet. The analyzer that uses it is the next PR in this stack. - Tests cover flattening of multiple changes with multiple URIs, an empty change list, and intersection with empty inputs, shared first, middle and last IDs, different lengths, IDs above the signed range (checking they order as unsigned), and distance being ignored. ## Test Plan `./tool/bazel test //submitqueue/extension/conflict/tango/...` `go test -race ./submitqueue/extension/conflict/tango/` `make check-gazelle` `make check-mocks` `make lint` CI ## Issue
1 parent c7a67dd commit 2993629

6 files changed

Lines changed: 263 additions & 1 deletion

File tree

‎submitqueue/extension/conflict/tango/BUILD.bazel‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,34 @@ go_library(
44
name = "go_default_library",
55
srcs = [
66
"cache.go",
7+
"intersect.go",
8+
"mock_request.go",
79
"mock_store.go",
10+
"request.go",
811
"signature.go",
912
"store.go",
1013
"target_label.go",
1114
"target_signature.go",
1215
],
1316
importpath = "github.com/uber/submitqueue/submitqueue/extension/conflict/tango",
1417
visibility = ["//visibility:public"],
15-
deps = ["@org_uber_go_mock//gomock:go_default_library"],
18+
deps = [
19+
"//platform/base/change:go_default_library",
20+
"@org_uber_go_mock//gomock:go_default_library",
21+
],
1622
)
1723

1824
go_test(
1925
name = "go_default_test",
2026
srcs = [
2127
"cache_test.go",
28+
"intersect_test.go",
29+
"request_test.go",
2230
"signature_test.go",
2331
],
2432
embed = [":go_default_library"],
2533
deps = [
34+
"//platform/base/change:go_default_library",
2635
"@com_github_stretchr_testify//assert:go_default_library",
2736
"@com_github_stretchr_testify//require:go_default_library",
2837
],
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2026 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 tango
16+
17+
// sharesTarget reports whether two signatures have a target ID in common. Both
18+
// must be sorted ascending by ID, so the check is one pass over each.
19+
func sharesTarget(a, b []SignatureTarget) bool {
20+
i, j := 0, 0
21+
for i < len(a) && j < len(b) {
22+
switch {
23+
case a[i].ID < b[j].ID:
24+
i++
25+
case a[i].ID > b[j].ID:
26+
j++
27+
default:
28+
return true
29+
}
30+
}
31+
return false
32+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2026 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 tango
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
)
22+
23+
func targetsWithIDs(ids ...uint64) []SignatureTarget {
24+
targets := make([]SignatureTarget, len(ids))
25+
for i, id := range ids {
26+
targets[i] = SignatureTarget{ID: id}
27+
}
28+
return targets
29+
}
30+
31+
func TestSharesTarget(t *testing.T) {
32+
const highBit = uint64(1) << 63
33+
34+
tests := []struct {
35+
name string
36+
a, b []SignatureTarget
37+
want bool
38+
}{
39+
{name: "both empty"},
40+
{name: "one empty", a: targetsWithIDs(1, 2)},
41+
{name: "disjoint", a: targetsWithIDs(1, 3, 5), b: targetsWithIDs(2, 4, 6)},
42+
{name: "same first ID", a: targetsWithIDs(1, 3), b: targetsWithIDs(1, 9), want: true},
43+
{name: "same last ID", a: targetsWithIDs(1, 7), b: targetsWithIDs(2, 7), want: true},
44+
{name: "same middle ID", a: targetsWithIDs(1, 4, 9), b: targetsWithIDs(2, 4, 8), want: true},
45+
{name: "different lengths", a: targetsWithIDs(5), b: targetsWithIDs(1, 2, 3, 4, 5, 6), want: true},
46+
{name: "IDs above the signed range order as unsigned", a: targetsWithIDs(1, highBit), b: targetsWithIDs(2, highBit+1)},
47+
{name: "same ID above the signed range", a: targetsWithIDs(1, highBit), b: targetsWithIDs(highBit), want: true},
48+
{
49+
name: "distance does not affect the result",
50+
a: []SignatureTarget{{ID: 4, Distance: 0}},
51+
b: []SignatureTarget{{ID: 4, Distance: 9}},
52+
want: true,
53+
},
54+
}
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
assert.Equal(t, tt.want, sharesTarget(tt.a, tt.b))
58+
assert.Equal(t, tt.want, sharesTarget(tt.b, tt.a))
59+
})
60+
}
61+
}

‎submitqueue/extension/conflict/tango/mock_request.go‎

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 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 tango
16+
17+
//go:generate mockgen -source=request.go -destination=mock_request.go -package=tango
18+
19+
import (
20+
"context"
21+
22+
"github.com/uber/submitqueue/platform/base/change"
23+
)
24+
25+
// ChangedTargetsRequest asks which targets change when ChangeURIs are applied
26+
// on top of a base revision.
27+
type ChangedTargetsRequest struct {
28+
// BaseRevision is the full commit SHA the changes are applied on top of.
29+
BaseRevision string
30+
31+
// ChangeURIs lists every change to apply, in batch order.
32+
ChangeURIs []string
33+
}
34+
35+
// ChangedTargetsClient returns the targets a request affects. An adapter owns
36+
// the Tango client and its stream, collects the response, and resolves each
37+
// target's label; any failure to produce a complete answer is an error. It also
38+
// owns the Tango repository remote and computation strategy, which the request
39+
// does not carry. ChangeURIs are passed through as given, so a URI scheme Tango
40+
// does not support is an error.
41+
type ChangedTargetsClient interface {
42+
// ChangedTargets returns the affected targets in no particular order, and a
43+
// label may repeat. An empty result is valid and means nothing is affected.
44+
ChangedTargets(ctx context.Context, request ChangedTargetsRequest) ([]LabelDistance, error)
45+
}
46+
47+
func changedTargetsRequestFor(baseRevision string, changes []change.Change) ChangedTargetsRequest {
48+
var uris []string
49+
for _, c := range changes {
50+
uris = append(uris, c.URIs...)
51+
}
52+
return ChangedTargetsRequest{BaseRevision: baseRevision, ChangeURIs: uris}
53+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2026 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 tango
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
22+
"github.com/uber/submitqueue/platform/base/change"
23+
)
24+
25+
func TestChangedTargetsRequestFor(t *testing.T) {
26+
baseRevision := "9f2c1d0e3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d"
27+
28+
tests := []struct {
29+
name string
30+
changes []change.Change
31+
want []string
32+
}{
33+
{name: "no changes", want: nil},
34+
{name: "one change", changes: []change.Change{{URIs: []string{"a"}}}, want: []string{"a"}},
35+
{
36+
name: "URIs are flattened in batch order",
37+
changes: []change.Change{{URIs: []string{"a", "b"}}, {URIs: []string{"c"}}, {URIs: []string{"d", "e"}}},
38+
want: []string{"a", "b", "c", "d", "e"},
39+
},
40+
}
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
got := changedTargetsRequestFor(baseRevision, tt.changes)
44+
45+
assert.Equal(t, ChangedTargetsRequest{
46+
BaseRevision: baseRevision,
47+
ChangeURIs: tt.want,
48+
}, got)
49+
})
50+
}
51+
}

0 commit comments

Comments
 (0)