Skip to content

Commit 36bdb7d

Browse files
committed
feat(stovepipe): define repair coordination contracts and RFC
1 parent 1a4571b commit 36bdb7d

24 files changed

Lines changed: 667 additions & 2 deletions

File tree

‎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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# First-failure repair submission
2+
3+
Build analysis records confirmed failures. The `fix` queue step selects the first eligible failed unit for each build and submits one repair through a Go extension. Submission can happen while the build is running. See the [workflow](workflow.md#workflow).
4+
5+
## 1. Publish a confirmed failure
6+
7+
[`buildanalysis.Controller.Process`](../../../stovepipe/controller/buildanalysis/buildanalysis.go) obtains confirmed [`BuildIssue`](../../../stovepipe/entity/build_issue.go) observations from [`buildissue.Detector`](../../../stovepipe/extension/buildissue/buildissue.go). The detector owns provider confirmation and retry rules.
8+
9+
The build-analysis integration persists the per-build observation through [`BuildIssueStore`](../../../stovepipe/extension/storage/build_issue_store.go) and reconciles its [`OpenIssue`](../../../stovepipe/entity/open_issue.go). Each continuous opening retains its immutable `FirstSeenBuildID`. A recurrence after recovery has a new opening build. Detecting builds and opening builds are separate identities.
10+
11+
Publish [`FixRequest`](../../../stovepipe/core/messagequeue/proto/fix.proto) with the queue, detecting BuildID, TaskID, UnitID, and FirstSeenBuildID. Partition by detecting BuildID. Replay publication after duplicate observation writes so interrupted handoffs can finish.
12+
13+
## 2. Select one issue per build
14+
15+
[`fix.Controller.Process`](../../../stovepipe/controller/fix/fix.go) reloads the Build and Request and checks queue identity. For a new attempt, it verifies the persisted failure and current issue opening.
16+
17+
1. Create a queued [`Fix`](../../../stovepipe/entity/fix.go) using the detecting BuildID as its queue-scoped ID. Create-if-absent selects the first eligible request processed, including when deliveries race.
18+
2. If that key already exists, reload its saved selection. Preparation retries use the saved issue, even when another target caused the delivery.
19+
3. Before dispatch, recheck the selected opening. If it recovered or reopened, mark the queued attempt skipped and retain the build's selection.
20+
21+
Build analysis continues collecting all failures. Later deliveries reconcile the saved submission and its history. Each build has its own initial attempt; cross-build deduplication and decisions about additional root causes are follow-up work.
22+
23+
## 3. Submit once
24+
25+
[`RepairExecutor.Submit`](../../../stovepipe/extension/fixer/fixer.go) accepts `SubmitRequest{BuildID, Issue}` and returns a nonempty opaque provider reference or an error. The executor is bound to its queue by its factory. Resolve it before claiming dispatch.
26+
27+
Conditionally persist `FixSubmitting` before calling Submit. Only the delivery whose conditional write succeeds calls the executor. Redelivery after that marker reconciles history and finishes without another trigger.
28+
29+
On acceptance, persist `FixSubmitted` and `ExecutionRef`. On an error or empty reference, persist `FixSubmissionUnknown` and the diagnostic in metadata. A trigger error can follow acceptance, so it leaves execution unknown. A failed result write leaves the submitting marker.
30+
31+
This is at-most-once dispatch. A crash between the durable marker and the outbound call can leave the repair unlaunched. A lost response leaves acceptance unknown. These cases require explicit follow-up. Execution monitoring can be added separately.
32+
33+
## 4. Record request history and provenance
34+
35+
Record `fix_submission_started` for the durable dispatch claim and `fix_submission_recorded` for a retained accepted or uncertain trigger result. Stable occurrence IDs derive from the fix ID. Replay reconstructs missing history from the saved Fix, including when a later target supplies the delivery. History failures never repeat submission. An attempt left submitting has only a started event.
36+
37+
[`FixStore`](../../../stovepipe/extension/storage/fix_store.go) adds one table with primary-key create, get, and conditional update. It retains the detecting BuildID, issue-opening references, submission state, optional provider reference and metadata, timestamps, and version. Build and OpenIssue keep their existing schemas.
38+
39+
Navigate `Fix.BuildID → Build.RequestID → Request.URI` for the initiating request and change. `Fix.Issues` retains opening identities after recovery. Build analysis owns failure and recovery evidence.
40+
41+
## Integration
42+
43+
[`core/fixrequest.Publish`](../../../stovepipe/core/fixrequest/publish.go) is the handoff after observation and positive open-issue persistence. The build-analysis owner supplies reconciliation and calls the helper, including on replay. The reference host retains its no-op detector until that integration lands.
44+
45+
The initial flow ends after submission and request-history recording. Execution monitoring, recovery correlation, and decisions about related failures can build on these records later.

‎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-seen build. Partition by detecting build ID. The `consumer.Controller` selects the first eligible failed unit by creating a Fix keyed by the detecting BuildID, persists a submitting marker, and calls `RepairExecutor.Submit` once. Later deliveries reconcile the selected submission and its request history.
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", FirstSeenBuildId: "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", FirstSeenBuildId: "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_seen_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_seen_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_seen_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)