diff --git a/submitqueue/extension/conflict/tango/BUILD.bazel b/submitqueue/extension/conflict/tango/BUILD.bazel new file mode 100644 index 000000000..10f193dd1 --- /dev/null +++ b/submitqueue/extension/conflict/tango/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "mock_store.go", + "store.go", + "target_label.go", + "target_signature.go", + ], + importpath = "github.com/uber/submitqueue/submitqueue/extension/conflict/tango", + visibility = ["//visibility:public"], + deps = ["@org_uber_go_mock//gomock:go_default_library"], +) diff --git a/submitqueue/extension/conflict/tango/mock_store.go b/submitqueue/extension/conflict/tango/mock_store.go new file mode 100644 index 000000000..8293d8f0d --- /dev/null +++ b/submitqueue/extension/conflict/tango/mock_store.go @@ -0,0 +1,205 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: store.go +// +// Generated by this command: +// +// mockgen -source=store.go -destination=mock_store.go -package=tango +// + +// Package tango is a generated GoMock package. +package tango + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockSignatureReader is a mock of SignatureReader interface. +type MockSignatureReader struct { + ctrl *gomock.Controller + recorder *MockSignatureReaderMockRecorder + isgomock struct{} +} + +// MockSignatureReaderMockRecorder is the mock recorder for MockSignatureReader. +type MockSignatureReaderMockRecorder struct { + mock *MockSignatureReader +} + +// NewMockSignatureReader creates a new mock instance. +func NewMockSignatureReader(ctrl *gomock.Controller) *MockSignatureReader { + mock := &MockSignatureReader{ctrl: ctrl} + mock.recorder = &MockSignatureReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignatureReader) EXPECT() *MockSignatureReaderMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSignatureReader) Get(ctx context.Context, batchID string) (TargetSignature, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, batchID) + ret0, _ := ret[0].(TargetSignature) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get. +func (mr *MockSignatureReaderMockRecorder) Get(ctx, batchID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSignatureReader)(nil).Get), ctx, batchID) +} + +// MockSignatureStore is a mock of SignatureStore interface. +type MockSignatureStore struct { + ctrl *gomock.Controller + recorder *MockSignatureStoreMockRecorder + isgomock struct{} +} + +// MockSignatureStoreMockRecorder is the mock recorder for MockSignatureStore. +type MockSignatureStoreMockRecorder struct { + mock *MockSignatureStore +} + +// NewMockSignatureStore creates a new mock instance. +func NewMockSignatureStore(ctrl *gomock.Controller) *MockSignatureStore { + mock := &MockSignatureStore{ctrl: ctrl} + mock.recorder = &MockSignatureStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignatureStore) EXPECT() *MockSignatureStoreMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockSignatureStore) Create(ctx context.Context, signature TargetSignature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, signature) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockSignatureStoreMockRecorder) Create(ctx, signature any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSignatureStore)(nil).Create), ctx, signature) +} + +// Get mocks base method. +func (m *MockSignatureStore) Get(ctx context.Context, batchID string) (TargetSignature, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, batchID) + ret0, _ := ret[0].(TargetSignature) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get. +func (mr *MockSignatureStoreMockRecorder) Get(ctx, batchID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSignatureStore)(nil).Get), ctx, batchID) +} + +// MockDictionaryReader is a mock of DictionaryReader interface. +type MockDictionaryReader struct { + ctrl *gomock.Controller + recorder *MockDictionaryReaderMockRecorder + isgomock struct{} +} + +// MockDictionaryReaderMockRecorder is the mock recorder for MockDictionaryReader. +type MockDictionaryReaderMockRecorder struct { + mock *MockDictionaryReader +} + +// NewMockDictionaryReader creates a new mock instance. +func NewMockDictionaryReader(ctrl *gomock.Controller) *MockDictionaryReader { + mock := &MockDictionaryReader{ctrl: ctrl} + mock.recorder = &MockDictionaryReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDictionaryReader) EXPECT() *MockDictionaryReaderMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDictionaryReader) Get(ctx context.Context, id uint64) (TargetLabel, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, id) + ret0, _ := ret[0].(TargetLabel) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get. +func (mr *MockDictionaryReaderMockRecorder) Get(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDictionaryReader)(nil).Get), ctx, id) +} + +// MockDictionaryStore is a mock of DictionaryStore interface. +type MockDictionaryStore struct { + ctrl *gomock.Controller + recorder *MockDictionaryStoreMockRecorder + isgomock struct{} +} + +// MockDictionaryStoreMockRecorder is the mock recorder for MockDictionaryStore. +type MockDictionaryStoreMockRecorder struct { + mock *MockDictionaryStore +} + +// NewMockDictionaryStore creates a new mock instance. +func NewMockDictionaryStore(ctrl *gomock.Controller) *MockDictionaryStore { + mock := &MockDictionaryStore{ctrl: ctrl} + mock.recorder = &MockDictionaryStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDictionaryStore) EXPECT() *MockDictionaryStoreMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockDictionaryStore) Create(ctx context.Context, label TargetLabel) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, label) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockDictionaryStoreMockRecorder) Create(ctx, label any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDictionaryStore)(nil).Create), ctx, label) +} + +// Get mocks base method. +func (m *MockDictionaryStore) Get(ctx context.Context, id uint64) (TargetLabel, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, id) + ret0, _ := ret[0].(TargetLabel) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get. +func (mr *MockDictionaryStoreMockRecorder) Get(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDictionaryStore)(nil).Get), ctx, id) +} diff --git a/submitqueue/extension/conflict/tango/store.go b/submitqueue/extension/conflict/tango/store.go new file mode 100644 index 000000000..0061bc5d7 --- /dev/null +++ b/submitqueue/extension/conflict/tango/store.go @@ -0,0 +1,60 @@ +// Copyright (c) 2026 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 tango + +//go:generate mockgen -source=store.go -destination=mock_store.go -package=tango + +import "context" + +// SignatureReader reads the target signature of a batch. It is the read-only +// view of SignatureStore for consumers that must not write. +type SignatureReader interface { + // Get returns the signature of batchID in the bound queue. The bool is + // false when no signature has been created for the batch. + Get(ctx context.Context, batchID string) (TargetSignature, bool, error) +} + +// SignatureStore persists target signatures. A signature is immutable: it is +// created once under (Queue, BatchID) and never updated. Reads and writes are +// per-batch point operations, so any backend can implement it. +type SignatureStore interface { + SignatureReader + + // Create persists signature in one write. Returns an already-exists error + // if the bound queue already holds a signature for signature.BatchID, so the + // first signature written stays and the caller can read it back with Get. + Create(ctx context.Context, signature TargetSignature) error +} + +// DictionaryReader reads the label recorded for a target ID. It is the +// read-only view of DictionaryStore. +type DictionaryReader interface { + // Get returns the label recorded for id in the bound queue. The bool is + // false when none has been recorded. + Get(ctx context.Context, id uint64) (TargetLabel, bool, error) +} + +// DictionaryStore persists the target dictionary: one TargetLabel for each +// 64-bit target ID. A record is immutable: it is created once under +// (Queue, ID) and never updated. Two labels can share an ID, so the first label +// recorded stays and the caller can read it back with Get to detect a collision. +type DictionaryStore interface { + DictionaryReader + + // Create persists label in one write. Returns an already-exists error if + // the bound queue holds a label for label.ID, whether or not it is the same + // label, so repeating a write is harmless. + Create(ctx context.Context, label TargetLabel) error +} diff --git a/submitqueue/extension/conflict/tango/target_label.go b/submitqueue/extension/conflict/tango/target_label.go new file mode 100644 index 000000000..2bfa70bd2 --- /dev/null +++ b/submitqueue/extension/conflict/tango/target_label.go @@ -0,0 +1,33 @@ +// Copyright (c) 2026 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 tango + +// TargetLabel is one build target label and the 64-bit ID it hashes to. The +// whole record is immutable: (Queue, ID) is its identity and there is no update +// path. Two labels can share an ID on a hash collision; the first one recorded +// stays. +type TargetLabel struct { + // Queue is the queue the label was seen in. Unique together with ID. + Queue string + + // ID is the 64-bit hash of Label. + ID uint64 + + // Label is the target label exactly as the build graph reported it. + Label string + + // CreatedAt is the Unix milliseconds timestamp when the record was created. + CreatedAt int64 +} diff --git a/submitqueue/extension/conflict/tango/target_signature.go b/submitqueue/extension/conflict/tango/target_signature.go new file mode 100644 index 000000000..fe6793c51 --- /dev/null +++ b/submitqueue/extension/conflict/tango/target_signature.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 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 tango + +// TargetSignature is the set of build targets a batch changes, as 64-bit IDs. +// The whole record is immutable: (Queue, BatchID) is its identity and there is +// no update path. +type TargetSignature struct { + // Queue is the queue the batch belongs to. Unique together with BatchID. + Queue string + + // BatchID is the canonical decimal batch identifier, unique within Queue. + BatchID string + + // BaseRevision is the commit SHA the changed targets were computed against. + BaseRevision string + + // Targets is sorted ascending by ID, with no duplicate IDs. + Targets []SignatureTarget + + // CreatedAt is the Unix milliseconds timestamp when the record was created. + CreatedAt int64 +} + +// SignatureTarget is one changed build target in a TargetSignature. +type SignatureTarget struct { + // ID is the 64-bit hash of the target's label. + ID uint64 + + // Distance is the distance from the nearest directly changed source in the + // reverse dependency graph; 0 means directly changed. + Distance int32 +}