Skip to content

Commit 8fbbaaf

Browse files
committed
feat(stovepipe): define repair coordination contracts and RFC
1 parent 303cb72 commit 8fbbaaf

27 files changed

Lines changed: 683 additions & 9 deletions

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ GO_GENERATE_PACKAGES := \
629629
./stovepipe/core/requestlog/... \
630630
./stovepipe/extension/buildissue/... \
631631
./stovepipe/extension/buildrunner/... \
632+
./stovepipe/extension/fixer/... \
632633
./stovepipe/extension/projectstatus/... \
633634
./stovepipe/extension/queueconfig/... \
634635
./stovepipe/extension/sourcecontrol/... \

‎doc/rfc/stovepipe/fixer.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# First-failure repair submission
2+
3+
Build analysis records confirmed failures. The `fix` queue step selects the first failed target for each build and triggers one repair through a Go extension. Later failed targets remain recorded and their fix requests no-op. Submission can happen while the build is running. See the [workflow](workflow.md#workflow).
4+
5+
## 1. Record the failure
6+
7+
[`buildanalysis.Controller.Process`](../../../stovepipe/controller/buildanalysis/buildanalysis.go) obtains confirmed `BuildIssue{TaskID, UnitID}` observations from [`buildissue.Detector`](../../../stovepipe/extension/buildissue/buildissue.go). The detector owns provider confirmation rules.
8+
9+
Persist the observation through [`BuildIssueStore`](../../../stovepipe/extension/storage/build_issue_store.go) and reconcile its [`OpenBuildIssue`](../../../stovepipe/entity/open_build_issue.go). Preserve the opening's `FirstObservedBuildID` and any `FixID` when updating observation fields. A reopened issue has a new opening build. Build IDs are opaque identities.
10+
11+
Publish [`FixRequest`](../../../stovepipe/core/messagequeue/proto/fix.proto) with the queue, detecting BuildID, TaskID, UnitID, and FirstObservedBuildID. Partition by detecting BuildID. Replay publication after duplicate observation writes so interrupted handoffs can finish.
12+
13+
## 2. Select one target for the build
14+
15+
[`fix.Controller.Process`](../../../stovepipe/controller/fix/fix.go) reloads the Build and Request, checks queue identity, and verifies the persisted failure and issue opening.
16+
17+
1. If `Build.FirstFixID` is empty, persist a queued [`Fix`](../../../stovepipe/entity/fix.go) and conditionally select it through the existing `Build.Version` guard.
18+
2. Keep the selected ID for the lifetime of the build. Requests for other targets acknowledge without submitting anything.
19+
3. A duplicate request for the selected target can finish preparation while the repair is still queued. Recheck the issue opening and persist its `FixID` while preserving observation fields.
20+
4. Mark an unselected competing candidate or a queued repair whose opening recovered as skipped. Preserve the build's selection.
21+
22+
The first eligible request processed wins. Build analysis continues collecting all observations. Each build has its own selection; cross-build or shared-cause deduplication requires later decision logic.
23+
24+
## 3. Trigger once
25+
26+
[`RepairExecutor.Submit(ctx, Fix) (string, error)`](../../../stovepipe/extension/fixer/fixer.go) is the provider interface. Resolve the executor and finish local preparation before claiming dispatch.
27+
28+
Conditionally persist `FixSubmitting` before making the outbound call. Only the delivery whose conditional write succeeds calls Submit. Redelivery after that marker acknowledges without another call, including after a timeout, process restart, or result-persistence failure.
29+
30+
On success, store `FixSubmitted` and the nonempty opaque `ExecutionRef`. On an error or empty reference, store `FixSubmissionUnknown` and the diagnostic in metadata. A trigger error can follow acceptance, so it does not establish execution failure. A failed result write leaves the durable submitting marker and does not repeat the outbound call.
31+
32+
This is at-most-once dispatch. A crash after the submitting marker and before the call may leave the repair unlaunched. A lost response leaves acceptance unknown. Both cases retain the build selection and require explicit follow-up rather than another automatic trigger. The provider's own execution behavior remains its responsibility.
33+
34+
## 4. Retain provenance
35+
36+
| Record | Key | Repair data |
37+
| --- | --- | --- |
38+
| Fix | queue/fix ID | Initiating BuildID, issue references, submission state, optional execution reference and metadata, version, timestamps. |
39+
| Existing Build | queue/build ID | Write-once `FirstFixID` under the existing snapshot version. |
40+
| OpenBuildIssue | queue/task/unit | Most recently selected `FixID` alongside the opening and observation state. |
41+
42+
Navigate `Fix.BuildID -> Build.RequestID -> Request.URI` for the initiating change. `Fix.Issues` retains the opening identity even if another build later assigns its own repair to that opening. Build analysis owns failure and recovery evidence.
43+
44+
The fixer adds one table and uses primary-key operations. Apply the schema additions and backfill the existing build/open-issue columns before deployment. Metadata contains optional provider details and submission diagnostics.
45+
46+
## Integration and follow-up work
47+
48+
[`core/fixrequest.Publish`](../../../stovepipe/core/fixrequest/publish.go) is the build-analysis handoff after observation and positive open-issue persistence. The build-analysis owner supplies that reconciliation and calls the helper, including on replay. The reference host retains its no-op detector until the integration lands.
49+
50+
The initial implementation ends after recording the trigger result. A future `fixsignal` step can observe execution and correlate authoritative recovery. A future decision policy can attach related failures to an existing repair or start additional fixes for unrelated causes. Those features are separate from this one-shot submission flow.

‎doc/rfc/stovepipe/workflow.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ Per-stage design detail lives under `steps/` so this doc stays a pipeline overvi
169169
- [buildsignal.md](steps/buildsignal.md) — the poll loop: hold-based re-poll cadence, per-build partitioning, and the fail-closed handoff to record
170170
- [record.md](steps/record.md) — turning a terminal build outcome into an immutable validation fact, monotonic last-green advancement and ref promotion, and the hook event announcing the outcome. Project analysis already runs inline through `projectstatus.Resolver` and may later use results calculated by build analysis
171171

172+
For repair decisions, execution, and recovery, see the [repair RFC](fixer.md).
173+
172174
## Dedup, idempotency, and history rewrites
173175

174176
Ingestion is idempotent on `(Queue, head URI)`, so duplicate poller reports — and any future webhook producer racing the poller — converge on one Request. The pipeline persists the Queue's last-green URI and per-URI greenness, so it must tolerate a **history rewrite**: when `SourceControl` reports that the last-green URI is no longer an ancestor of the current head, `process` falls back to a full-monorepo build rather than trusting a baseline that no longer exists on the branch. The system converges to a correct greenness rather than wedging on a stale pointer.

‎stovepipe/core/messagequeue/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ Payloads are defined in proto3 (`proto/`, generated into `protopb/`) and seriali
1111
- **buildsignal** (`TopicKeyBuildSignal`, `BuildSignal`) — build publishes the build id here after triggering; buildsignal holds the delivery between polls until the build reaches a terminal status. Partitioned by build id, so each build's poll loop is an independent partition.
1212
- **buildanalysis** (`TopicKeyBuildAnalysis`, `BuildAnalysis`) — identifies a persisted build for issue detection, partitioned by build id. Build publishes a start notice and buildsignal publishes a completion notice with a distinct message id. See [build analysis](../../controller/buildanalysis/README.md) for polling and persistence behavior.
1313
- **record** (`TopicKeyRecord`, `Record`) — buildsignal publishes the request id after persisting the terminal outcome; record reloads the request and records its validation fact. Partitioned by request id.
14+
15+
## Repair stage contract
16+
17+
**fix** (`TopicKeyFix`, [`FixRequest`](proto/fix.proto)) identifies a persisted observation and issue opening using queue, detecting build, task, unit, and first-observed build. Partition by detecting build ID. The `consumer.Controller` selects the first failed target through `Build.FirstFixID`, persists a submitting marker, and calls `RepairExecutor.Submit` once. Later failed targets no-op.
18+
19+
Persist observations and their current opening before publishing a request. Replay publication after duplicate observation writes so a crash between persistence and handoff can recover. Redelivery can complete preparation, but a durable submitting marker suppresses any further outbound call. Execution monitoring and verified recovery are follow-up work.

‎stovepipe/core/messagequeue/messagequeue.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ type (
5656
// Record is the payload buildsignal publishes to the record stage once a
5757
// build reaches a terminal status: the build id to record.
5858
Record = protopb.Record
59+
60+
// FixRequest identifies a confirmed issue opening and the detecting build.
61+
FixRequest = protopb.FixRequest
5962
)
6063

6164
// marshalOpts keeps the JSON field names identical to the proto field names

‎stovepipe/core/messagequeue/messagequeue_test.go‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,40 @@ func TestRecordRoundTrip(t *testing.T) {
6666
assert.True(t, proto.Equal(rec, got), "round-tripped Record should equal the original")
6767
}
6868

69+
func TestFixMessagesRoundTrip(t *testing.T) {
70+
for _, tt := range []struct {
71+
name string
72+
message proto.Message
73+
}{
74+
{
75+
name: "issue request",
76+
message: &FixRequest{
77+
QueueName: "repo/main",
78+
BuildId: "build-42", TaskId: "compile", UnitId: "package", FirstObservedBuildId: "build-41",
79+
},
80+
},
81+
} {
82+
t.Run(tt.name, func(t *testing.T) {
83+
data, err := Marshal(tt.message)
84+
require.NoError(t, err)
85+
got := tt.message.ProtoReflect().New().Interface()
86+
require.NoError(t, Unmarshal(data, got))
87+
assert.True(t, proto.Equal(tt.message, got))
88+
})
89+
}
90+
}
91+
92+
func TestFixRequestWireFormat(t *testing.T) {
93+
message := &FixRequest{QueueName: "repo/main", BuildId: "build-42", TaskId: "check-a", FirstObservedBuildId: "build-41"}
94+
data, err := Marshal(message)
95+
require.NoError(t, err)
96+
assert.JSONEq(t, `{"queue_name":"repo/main","build_id":"build-42","task_id":"check-a","first_observed_build_id":"build-41"}`, string(data))
97+
98+
var got FixRequest
99+
require.NoError(t, Unmarshal([]byte(`{"queue_name":"repo/main","build_id":"build-42","task_id":"check-a","first_observed_build_id":"build-41","future_field":true}`), &got))
100+
assert.True(t, proto.Equal(message, &got))
101+
}
102+
69103
// TestWireFormat locks the protojson encoding decision the contract relies on:
70104
// snake_case field names (UseProtoNames).
71105
func TestWireFormat(t *testing.T) {
@@ -80,7 +114,7 @@ func TestWireFormat(t *testing.T) {
80114
// no topic_keys option names an unknown key.
81115
func TestTopicKeysBindEveryTopicKey(t *testing.T) {
82116
bound := map[string]int{}
83-
for _, m := range []proto.Message{&ProcessRequest{}, &BuildRequest{}, &BuildSignal{}, &BuildAnalysis{}, &Record{}} {
117+
for _, m := range []proto.Message{&ProcessRequest{}, &BuildRequest{}, &BuildSignal{}, &BuildAnalysis{}, &Record{}, &FixRequest{}} {
84118
keys := TopicKeys(m)
85119
require.NotEmpty(t, keys, "message must declare a non-empty topic_keys option")
86120
for _, key := range keys {
@@ -94,6 +128,7 @@ func TestTopicKeysBindEveryTopicKey(t *testing.T) {
94128
TopicKeyBuildSignal,
95129
TopicKeyBuildAnalysis,
96130
TopicKeyRecord,
131+
TopicKeyFix,
97132
}
98133

99134
valid := map[string]bool{}

‎stovepipe/core/messagequeue/proto/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ exports_files(
33
"build.proto",
44
"buildanalysis.proto",
55
"buildsignal.proto",
6+
"fix.proto",
67
"process.proto",
78
"record.proto",
89
],
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
syntax = "proto3";
16+
17+
package uber.stovepipe.messagequeue;
18+
19+
import "api/base/messagequeue/proto/messagequeue.proto";
20+
21+
option go_package = "github.com/uber/submitqueue/stovepipe/core/messagequeue/protopb";
22+
option java_multiple_files = true;
23+
option java_outer_classname = "FixRequestProto";
24+
option java_package = "com.uber.submitqueue.stovepipe.messagequeue";
25+
26+
// FixRequest identifies a confirmed issue opening and the build requesting repair.
27+
// The observation and open issue must be persisted before publication.
28+
message FixRequest {
29+
option (uber.base.messagequeue.topic_keys) = "fix";
30+
31+
string queue_name = 1;
32+
string build_id = 2;
33+
string task_id = 3;
34+
string unit_id = 4;
35+
string first_observed_build_id = 5;
36+
}

‎stovepipe/core/messagequeue/protopb/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ go_library(
66
"build.pb.go",
77
"buildanalysis.pb.go",
88
"buildsignal.pb.go",
9+
"fix.pb.go",
910
"process.pb.go",
1011
"record.pb.go",
1112
],

‎stovepipe/core/messagequeue/protopb/fix.pb.go‎

Lines changed: 179 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)