Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions api/stovepipe/proto/stovepipe.proto
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ message HistoryEvent {
// Event that occurred without changing the request state.
string event = 4;
}
// Newer request that caused a superseded state. Empty when not applicable.
string superseded_by_request_id = 5;
// Build associated with the occurrence. Empty when no build applies.
string build_id = 6;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These haven't been used yet so deleting them here in backwards incompatible way. They're already unused elsewhere in the code/db schema.

// Stable domain reason for a terminal request outcome. Empty otherwise.
string outcome_reason = 7;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either change this ordinal to 5 (which will make it a breaking change) or reserve 5 and 6

}
Expand Down
24 changes: 2 additions & 22 deletions api/stovepipe/protopb/stovepipe.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 36 additions & 39 deletions api/stovepipe/protopb/stovepipe.pb.yarpc.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 9 additions & 11 deletions doc/rfc/stovepipe/request-history-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SubmitQueue's URI method returns several histories because the same change may b

## Representative Contract

The final protobuf receives a separate compatibility review before implementation. Its representative shape is:
The protobuf contract is:

```proto
message GetRequestHistoryByIDRequest {
Expand All @@ -39,8 +39,6 @@ message HistoryEvent {
string request_state = 3;
string event = 4;
}
string superseded_by_request_id = 5;
string build_id = 6;
string outcome_reason = 7;
}

Expand Down Expand Up @@ -69,11 +67,11 @@ Event IDs are opaque. Clients may compare them but never parse their format. Req

## Selection Flow

Request-ID lookup validates the queue and ID, loads the queue's `Request` to validate the selector, and lists its `RequestLogStore` records. Its response projects only the ordered events, matching SubmitQueue's equivalent API.
Request-ID lookup validates the queue and ID and lists the queue's `RequestLogStore` records. At least one retained log record defines the existence of a history; the API does not consult the operational `Request` entity. Its response projects only the ordered events, matching SubmitQueue's equivalent API.

URI lookup currently resolves one request ID from the existing `RequestURIStore` primary key and delegates to the same request-ID path. Supporting multiple retained attempts later requires the URI index to enumerate their request IDs; each history still uses primary-key reads for its Request and log. The API does not scan the request log by URI. A missing mapping is not found; a mapping whose Request is missing is an internal consistency error.
URI lookup currently resolves one request ID from the existing `RequestURIStore` primary key and delegates to the same retained-log read. Supporting multiple retained attempts later requires the URI index to enumerate their request IDs; each history still uses a primary-key log read. The API does not scan the request log by URI. A missing mapping or a mapped request ID with no retained log is not found.

Every request-URI mapping must be repaired and retained with its Request and history. Otherwise URI lookup could lose coverage while request-ID lookup still succeeds. Each `RequestHistory` contains only the selected request ID and its events; URI, build strategy, and base URI remain resolvable from the Request rather than being duplicated in the history response.
Every request-URI mapping must be retained for the same advertised period as its history. Otherwise URI lookup could lose coverage while request-ID lookup still succeeds. Each `RequestHistory` contains only the selected request ID and its events; operational request context is not duplicated in the history response.

## Public Projection

Expand All @@ -92,9 +90,9 @@ History events representing state changes set `request_state` and preserve each

These strings intentionally match the current domain states one-to-one, but they are a stable public history vocabulary: an internal refactor cannot rename or reinterpret an existing wire value. In particular, `succeeded` and `failed` remain distinct rather than collapsing into a derived snapshot phase such as `finalizing`.

Build events set `event` and decode `build_id` from the reserved request-log metadata key. `validation_fact_recorded` records that a fact was established, while its degree remains internal metadata until a typed public projection is designed. Superseded state events similarly decode `superseded_by_request_id`. The `occurrence` oneof makes state and event mutually exclusive without a redundant type field. A terminal Request state entry never substitutes for the fact event.
Build and validation-fact occurrences set `event`. The `occurrence` oneof makes state and event mutually exclusive without a redundant type field. A terminal Request state entry never substitutes for the fact event.

The raw `RequestLog.Metadata` map, unknown metadata keys, dependency errors, credentials, and stack traces are not exposed. `outcome_reason` uses a bounded public vocabulary.
The raw `RequestLog.Metadata` map, request version, dependency errors, credentials, and stack traces are not exposed. Typed metadata projections can be added later when a concrete client need defines their contract. `outcome_reason` uses a bounded public vocabulary.

## Materialization

Expand Down Expand Up @@ -123,13 +121,13 @@ Request-ID lookup and the corresponding history within URI lookup return the sam

Request-ID lookup returns all currently retained events for the selected request. URI lookup returns every retained request history associated with the exact URI. This matches SubmitQueue's request-history response shapes. The bounded lifecycle vocabulary and reasonable per-request build limits keep each history modest.

History, `Request`, and request-URI mapping retention must support the same advertised lookup period. The API does not promise a lifetime longer than every record required by its selector. The initial rollout exposes only requests accepted after all history writers and repair paths are active; older requests are outside the lookup period and are not backfilled.
History and request-URI mapping retention must support the same advertised lookup period. The API does not promise a lifetime longer than every record required by its selector. The initial rollout exposes only requests accepted after all history writers and repair paths are active; older requests without retained logs are outside the lookup period and are not backfilled.

## Errors and Authorization

- Empty queue or selector is invalid.
- An unknown request ID or URI, including one scoped to the wrong queue, is not found.
- A URI mapping whose Request is missing and a request within the advertised history lookup period whose required history is missing are internal consistency errors.
- A request ID or URI with no retained history is not found, including a URI mapping whose request ID has no retained log rows.
- Retryable storage failures are unavailable; context cancellation and deadline errors retain their canonical codes.

Authorization follows the same queue policy as other Stovepipe reads. Possession of a request ID alone does not bypass queue authorization.
Expand All @@ -142,7 +140,7 @@ Contract and controller tests cover:
- deterministic equal-timestamp ordering;
- state, build-event, and fact-event public mapping;
- queue isolation and cross-queue not found;
- unknown selectors versus dangling mappings;
- unknown selectors and URI mappings without retained logs;
- a repaired older occurrence appearing in chronological position;
- unknown future request-state and event strings remaining readable.

Expand Down
10 changes: 8 additions & 2 deletions service/stovepipe/server/mapper/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["ingest.go"],
srcs = [
"ingest.go",
"request_history.go",
],
importpath = "github.com/uber/submitqueue/service/stovepipe/server/mapper",
visibility = ["//visibility:public"],
deps = [
Expand All @@ -13,7 +16,10 @@ go_library(

go_test(
name = "go_default_test",
srcs = ["ingest_test.go"],
srcs = [
"ingest_test.go",
"request_history_test.go",
],
embed = [":go_default_library"],
deps = [
"//api/stovepipe/protopb:go_default_library",
Expand Down
61 changes: 61 additions & 0 deletions service/stovepipe/server/mapper/request_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2025 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 mapper

import (
pb "github.com/uber/submitqueue/api/stovepipe/protopb"
"github.com/uber/submitqueue/stovepipe/entity"
)

// ProtoToGetRequestHistoryByIDRequest maps a wire request to its domain value.
func ProtoToGetRequestHistoryByIDRequest(req *pb.GetRequestHistoryByIDRequest) entity.GetRequestHistoryByIDRequest {
return entity.GetRequestHistoryByIDRequest{ID: req.GetRequestId(), Queue: req.GetQueue()}
}

// ProtoToGetRequestHistoryByURIRequest maps a wire request to its domain value.
func ProtoToGetRequestHistoryByURIRequest(req *pb.GetRequestHistoryByURIRequest) entity.GetRequestHistoryByURIRequest {
return entity.GetRequestHistoryByURIRequest{URI: req.GetUri(), Queue: req.GetQueue()}
}

// HistoryEventsToProto maps retained request-log events to wire history events.
func HistoryEventsToProto(logs []entity.RequestLog) []*pb.HistoryEvent {
events := make([]*pb.HistoryEvent, len(logs))
for i, log := range logs {
event := &pb.HistoryEvent{
EventId: log.ID,
TimestampMs: log.TimestampMs,
OutcomeReason: string(log.OutcomeReason),
}
if log.State != entity.RequestStateUnknown {
event.Occurrence = &pb.HistoryEvent_RequestState{RequestState: string(log.State)}
} else {
event.Occurrence = &pb.HistoryEvent_Event{Event: string(log.Event)}
}
events[i] = event
}
return events
}

// RequestHistoriesToProto maps grouped retained histories to their wire representation.
func RequestHistoriesToProto(histories []entity.RequestHistory) []*pb.RequestHistory {
result := make([]*pb.RequestHistory, len(histories))
for i, history := range histories {
result[i] = &pb.RequestHistory{
RequestId: history.RequestID,
Events: HistoryEventsToProto(history.Events),
}
}
return result
}
Loading
Loading