Skip to content

Commit dfd00a9

Browse files
committed
feat(stovepipe): Add request issue lookups
1 parent ae8d7c3 commit dfd00a9

23 files changed

Lines changed: 1286 additions & 146 deletions

‎api/stovepipe/proto/stovepipe.proto‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,43 @@ message ListOpenIssuesResponse {
375375
string next_page_token = 2;
376376
}
377377

378+
// RequestIssue describes one accepted issue update, not the queue's open-issue set.
379+
message RequestIssue {
380+
// Logical validation task.
381+
string task_id = 1;
382+
// Validation unit; empty identifies a task-wide failure.
383+
string unit_id = 2;
384+
// "new" (including reopened), "existing" (reported and already open), or "closed".
385+
string kind = 3;
386+
// Build whose accepted update produced this result.
387+
string build_id = 4;
388+
// Build where this occurrence first opened.
389+
string first_seen_build_id = 5;
390+
}
391+
392+
message GetIssuesByRequestIDRequest {
393+
// Required configured queue.
394+
string queue = 1;
395+
// Required canonical positive decimal request ID within the queue.
396+
string request_id = 2;
397+
// Maximum results to return. Zero selects 50; otherwise 1 through 200.
398+
int32 page_size = 3;
399+
// Opaque continuation bound to this queue and request.
400+
string page_token = 4;
401+
}
402+
403+
// Completed and abandoned results are stable; in-progress pages are not a snapshot.
404+
message GetIssuesByRequestIDResponse {
405+
// Results in ascending bytewise task/unit order, classified as new, existing, or closed.
406+
repeated RequestIssue issues = 1;
407+
// "in_progress", "complete", "incomplete" (abandoned), or "unavailable".
408+
// Unavailable means recording has not started or predates retained coverage.
409+
// Complete describes reconciliation, not build success or absence of open issues.
410+
string status = 2;
411+
// Empty ends this scan. Restart in-progress scans after completion to include late results.
412+
string next_page_token = 3;
413+
}
414+
378415
// Stovepipe provides the Stovepipe API.
379416
service Stovepipe {
380417
// Ping returns a response indicating the service is alive
@@ -396,4 +433,6 @@ service Stovepipe {
396433
rpc GetQueueStatus(GetQueueStatusRequest) returns (GetQueueStatusResponse) {}
397434
// ListOpenIssues returns current open issues validated against their summaries.
398435
rpc ListOpenIssues(ListOpenIssuesRequest) returns (ListOpenIssuesResponse) {}
436+
// GetIssuesByRequestID returns accepted issue results for one request.
437+
rpc GetIssuesByRequestID(GetIssuesByRequestIDRequest) returns (GetIssuesByRequestIDResponse) {}
399438
}

‎api/stovepipe/protopb/stovepipe.pb.go‎

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

‎api/stovepipe/protopb/stovepipe.pb.yarpc.go‎

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

‎api/stovepipe/protopb/stovepipe_grpc.pb.go‎

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Request issues API
2+
3+
The request-issues lookup returns accepted results for a queue and request ID, not the queue's full open-issue set. Each result is new (opened or reopened), existing (reported but already open), or closed (actually closed by this request). Older issues retained because closure was suppressed are not existing results unless this request reported them.
4+
5+
Results are read from immutable request-issue rows under the queue/request primary-key prefix, in task/unit order. The default page size is 50 and the maximum is 200. A queue- and request-bound opaque continuation resumes after the last returned key; one extra row establishes whether another page exists. No joins or secondary indexes are required.
6+
7+
The response reports in-progress recording, complete reconciliation, incomplete abandoned work, or unavailable coverage. Completion is read before the result page and is written only after all accepted result writes finish. Completed or abandoned results remain stable as later requests update the queue. Complete does not mean the build passed or all queue issues closed; canceled builds and task-wide failures can suppress closure.
8+
9+
In-progress pages are not a snapshot: concurrent recording can add keys behind the cursor. Restart from the first page once recording finishes. Unavailable means recording has not started or the request predates the retained start marker; an empty page then does not establish that the request had no issues. Historical results are not backfilled.
10+
11+
Recording coverage assumes every issue-tracking worker writes request results. Drain older workers before admitting requests whose summaries need complete coverage; requests started before that deployment boundary are not backfilled.

‎service/stovepipe/server/handler/BUILD.bazel‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ go_library(
1111
"//stovepipe/controller:go_default_library",
1212
"//stovepipe/core/queuelog:go_default_library",
1313
"//stovepipe/core/queuestate:go_default_library",
14+
"//stovepipe/extension/storage:go_default_library",
1415
"@org_golang_google_grpc//codes:go_default_library",
1516
"@org_golang_google_grpc//status:go_default_library",
1617
],
@@ -19,6 +20,7 @@ go_library(
1920
go_test(
2021
name = "go_default_test",
2122
srcs = [
23+
"get_issues_by_request_id_test.go",
2224
"list_open_issues_test.go",
2325
"list_test.go",
2426
"queue_status_test.go",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 handler
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
pb "github.com/uber/submitqueue/api/stovepipe/protopb"
24+
"github.com/uber/submitqueue/stovepipe/controller"
25+
controllermock "github.com/uber/submitqueue/stovepipe/controller/mock"
26+
"github.com/uber/submitqueue/stovepipe/entity"
27+
"github.com/uber/submitqueue/stovepipe/extension/storage"
28+
"go.uber.org/mock/gomock"
29+
"google.golang.org/grpc/codes"
30+
"google.golang.org/grpc/status"
31+
)
32+
33+
func TestGetIssuesByRequestID(t *testing.T) {
34+
for _, tt := range []struct {
35+
name string
36+
err error
37+
code codes.Code
38+
}{
39+
{"success", nil, codes.OK}, {"invalid", controller.ErrInvalidRequest, codes.InvalidArgument},
40+
{"missing request", storage.ErrNotFound, codes.NotFound}, {"canceled", context.Canceled, codes.Canceled},
41+
{"deadline", context.DeadlineExceeded, codes.DeadlineExceeded}, {"dependency", assert.AnError, codes.Unknown},
42+
} {
43+
t.Run(tt.name, func(t *testing.T) {
44+
c := controllermock.NewMockGetIssuesByRequestIDController(gomock.NewController(t))
45+
c.EXPECT().GetIssuesByRequestID(gomock.Any(), entity.GetIssuesByRequestIDRequest{Queue: "queue", RequestID: "12", PageSize: 10, PageToken: "token"}).Return(entity.GetIssuesByRequestIDResult{Status: entity.RequestIssuesStatusIncomplete, NextPageToken: "next"}, tt.err)
46+
server := NewStovepipeServer(nil, nil, nil, nil, nil, nil, nil, c)
47+
response, err := server.GetIssuesByRequestID(context.Background(), &pb.GetIssuesByRequestIDRequest{Queue: "queue", RequestId: "12", PageSize: 10, PageToken: "token"})
48+
require.Equal(t, tt.code, status.Code(err))
49+
if tt.err == nil {
50+
require.Equal(t, "incomplete", response.Status)
51+
require.Equal(t, "next", response.NextPageToken)
52+
} else {
53+
require.Nil(t, response)
54+
}
55+
})
56+
}
57+
}

‎service/stovepipe/server/handler/list_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestList(t *testing.T) {
6868
}, req)
6969
return tt.result, tt.err
7070
}}
71-
srv := NewStovepipeServer(nil, nil, nil, nil, fake, nil, nil)
71+
srv := NewStovepipeServer(nil, nil, nil, nil, fake, nil, nil, nil)
7272
response, err := srv.List(ctx, &pb.ListRequest{
7373
Queue: "queue", PageSize: 25, PageToken: "token",
7474
AcceptedLowerBound: &pb.ListRequest_AcceptedAtOrAfterMs{},

‎service/stovepipe/server/handler/queue_status_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestGetQueueStatus(t *testing.T) {
5353
require.Equal(t, entity.GetQueueStatusRequest{Queue: "repo/main", ChangeURI: "C"}, req)
5454
return entity.GetQueueStatusResult{Queue: "repo/main", Enablement: entity.QueueState{Version: 1, State: entity.QueueEnablementDisabled}}, wantErr
5555
}}
56-
server := NewStovepipeServer(nil, nil, nil, nil, nil, fake, nil)
56+
server := NewStovepipeServer(nil, nil, nil, nil, nil, fake, nil, nil)
5757
response, err := server.GetQueueStatus(context.Background(), &pb.GetQueueStatusRequest{Queue: "repo/main", ChangeUri: "C"})
5858
if wantErr != nil {
5959
require.Equal(t, tt.code, status.Code(err))

‎service/stovepipe/server/handler/server.go‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/uber/submitqueue/stovepipe/core/queuelog"
2323
"github.com/uber/submitqueue/stovepipe/core/queuestate"
24+
"github.com/uber/submitqueue/stovepipe/extension/storage"
2425
"google.golang.org/grpc/codes"
2526
"google.golang.org/grpc/status"
2627

@@ -38,6 +39,7 @@ type StovepipeServer struct {
3839
projectStatusController controller.ProjectStatusController
3940
listController controller.ListController
4041
openIssuesController controller.ListOpenIssuesController
42+
requestIssuesController controller.GetIssuesByRequestIDController
4143
queueStatusController controller.GetQueueStatusController
4244
}
4345

@@ -50,6 +52,7 @@ func NewStovepipeServer(
5052
listController controller.ListController,
5153
queueStatusController controller.GetQueueStatusController,
5254
openIssuesController controller.ListOpenIssuesController,
55+
requestIssuesController controller.GetIssuesByRequestIDController,
5356
) *StovepipeServer {
5457
return &StovepipeServer{
5558
pingController: pingController,
@@ -58,6 +61,7 @@ func NewStovepipeServer(
5861
projectStatusController: projectStatusController,
5962
listController: listController,
6063
openIssuesController: openIssuesController,
64+
requestIssuesController: requestIssuesController,
6165
queueStatusController: queueStatusController,
6266
}
6367
}
@@ -161,3 +165,21 @@ func (s *StovepipeServer) ListOpenIssues(ctx context.Context, req *pb.ListOpenIs
161165
}
162166
return mapper.ListOpenIssuesResultToProto(result), nil
163167
}
168+
169+
// GetIssuesByRequestID returns accepted results and their recording status.
170+
func (s *StovepipeServer) GetIssuesByRequestID(ctx context.Context, req *pb.GetIssuesByRequestIDRequest) (*pb.GetIssuesByRequestIDResponse, error) {
171+
result, err := s.requestIssuesController.GetIssuesByRequestID(ctx, mapper.ProtoToGetIssuesByRequestIDRequest(req))
172+
if err != nil {
173+
switch {
174+
case controller.IsInvalidRequest(err):
175+
return nil, status.Error(codes.InvalidArgument, err.Error())
176+
case errors.Is(err, storage.ErrNotFound):
177+
return nil, status.Error(codes.NotFound, err.Error())
178+
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
179+
return nil, status.FromContextError(err).Err()
180+
default:
181+
return nil, err
182+
}
183+
}
184+
return mapper.GetIssuesByRequestIDResultToProto(result), nil
185+
}

0 commit comments

Comments
 (0)