Skip to content

Commit 922cb26

Browse files
committed
docs(rfc): conclude request failures
Summary: Broaden the existing conclude topic to accept structured request failures from business-decision controllers and DLQs. Document the tagged payload contract, producer behavior, replay semantics, and rollout. Test Plan: - make lint - make check-tidy - make check-gazelle
1 parent 39d61fe commit 922cb26

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

‎doc/rfc/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting
1515
- [Gateway History APIs](submitqueue/history-api.md) - Request lifecycle history exposed through separate request ID and change ID endpoints
1616
- [Build Runner](submitqueue/build-runner.md) - Vendor-agnostic BuildRunner interface, provider-neutral BuildStatus lifecycle, and how the orchestrator wires it into the build stage
1717
- [Extension Contract](submitqueue/extension-contract.md) - When extensions take orchestrator identity (request/batch) and resolve granular content themselves vs. take controller-resolved data; revises the BuildRunner base/head contract
18+
- [Conclude Request Failures](submitqueue/conclude-request-failures.md) - Broaden the existing conclude topic to finalize structured business rejections and permanent controller failures
1819
- [Gateway Status and List APIs](submitqueue/status-list-api.md) - Gateway-owned request context, materialized current status, sqid or change-URI status lookup, and queue admission listing
1920
- [Speculation](submitqueue/speculation.md) - Why SubmitQueue speculates, the path/tree model, and the two pluggable seams: speculation-tree enumeration and path selection
2021
- [Modular Queue Wiring](submitqueue/modular-queue-wiring.md) - Declare-don't-assemble engine (`pipeline.Construct`) that unifies topic registry, controller registration, DLQ pairing, and lifecycle ordering into one typed call; services self-declare via Deps struct + Stages slice, hosts own per-queue profiles and transport
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Conclude Request Failures
2+
3+
Broaden the existing `conclude` stage so controllers and DLQs can send it structured request failures. Do not add a separate `request-failure` topic.
4+
5+
## Current versus proposed
6+
7+
Current `conclude` consumes a `BatchID`, loads the terminal batch, updates each member request, and publishes its terminal request log.
8+
9+
The proposal keeps that path and adds `RequestFailureID` as a second tagged input:
10+
11+
| | Current | Proposed |
12+
|---|---|---|
13+
| Input | `BatchID` | `BatchID` or `RequestFailureID` |
14+
| Source | Terminal batch | Terminal batch, business rejection, or DLQ |
15+
| Context | Batch state and ID | Structured stage, code, message, and metadata |
16+
| Failure ownership | Controllers and DLQs may terminalize directly | They delegate terminalization and logging to `conclude` |
17+
18+
The finalization algorithm is not new. The change is a broader input contract and durable failure context.
19+
20+
```
21+
terminal batch ────────────────────────────────> conclude { batch_id }
22+
23+
known business rejection ─> RequestFailure ────> conclude { failure_id }
24+
25+
permanent controller error ─> DLQ ─> RequestFailure ─> conclude { failure_id }
26+
```
27+
28+
Both business-decision controllers and DLQs publish to `conclude`. A controller publishes only after reaching a known terminal decision. Execution and infrastructure errors continue to return as errors while recovery remains possible; the DLQ publishes only after they become permanent.
29+
30+
## Contracts
31+
32+
`ConcludeMessage` is an internal protobuf with a tagged `oneof`:
33+
34+
```proto
35+
message ConcludeMessage {
36+
oneof target {
37+
string batch_id = 1;
38+
string request_failure_id = 2;
39+
}
40+
}
41+
```
42+
43+
This avoids mixing bare `BatchID` and `RequestFailureID` payloads on one topic.
44+
45+
`RequestFailure` is an immutable orchestrator entity containing a stable ID, request ID, stage, kind (`business_rejection` or `controller_failure`), stable code, message, metadata, and creation time. Producers create it idempotently; the queue carries only its ID.
46+
47+
## Controller change
48+
49+
Today a controller such as `mergeconflictsignal` terminalizes a business rejection directly:
50+
51+
```go
52+
if result.Outcome != runwaypb.Outcome_SUCCEEDED {
53+
return c.failRequest(ctx, request, result.Reason)
54+
}
55+
```
56+
57+
Under this proposal it persists the decision and delegates finalization:
58+
59+
```go
60+
if result.Outcome != runwaypb.Outcome_SUCCEEDED {
61+
failure := entity.NewRequestFailure(
62+
request.ID,
63+
"mergeconflictsignal",
64+
entity.FailureKindBusinessRejection,
65+
"mergeability_check_failed",
66+
result.Reason,
67+
metadata,
68+
)
69+
if err := failureStore.Create(ctx, failure); err != nil {
70+
return fmt.Errorf("create request failure: %w", err)
71+
}
72+
return publishConcludeFailure(ctx, failure.ID, request.Queue)
73+
}
74+
```
75+
76+
The controller no longer changes `Request.State` or publishes a terminal request log.
77+
78+
## DLQ change
79+
80+
Today a request-scoped DLQ changes the request to `RequestStateError` and publishes a log using `dlq.last_error`.
81+
82+
Under this proposal it creates a generic durable failure and publishes it to `conclude`:
83+
84+
```go
85+
failure := entity.NewRequestFailure(
86+
requestID,
87+
originalTopic,
88+
entity.FailureKindControllerFailure,
89+
"controller_failure",
90+
delivery.Metadata()["dlq.last_error"],
91+
nil,
92+
)
93+
if err := failureStore.Create(ctx, failure); err != nil {
94+
return err
95+
}
96+
return publishConcludeFailure(ctx, failure.ID, partitionKey)
97+
```
98+
99+
A previously persisted business failure wins over a later generic DLQ failure for the same logical transition. A batch-scoped DLQ still marks the batch `BatchStateFailed`, then publishes one failure conclusion per member to preserve the original error context.
100+
101+
## Broadened conclude
102+
103+
`conclude` dispatches on the tagged reference:
104+
105+
```go
106+
switch target := msg.Target.(type) {
107+
case *ConcludeMessage_BatchId:
108+
return c.concludeBatch(ctx, target.BatchId)
109+
case *ConcludeMessage_RequestFailureId:
110+
return c.concludeFailure(ctx, target.RequestFailureId)
111+
default:
112+
return errors.New("conclude message has no target")
113+
}
114+
```
115+
116+
- `concludeBatch` retains the current batch fan-out behavior.
117+
- `concludeFailure` loads the failure and request, conditionally changes the request to `RequestStateError`, and publishes `RequestStatusError` with the structured context.
118+
- Both paths use the same request reconciliation and log-repair helper.
119+
- Gateway remains the sole writer of the request-log database.
120+
121+
## Replay and rollout
122+
123+
- Failure creation and conclusion publication use stable identities.
124+
- If state update succeeds but log publication fails, redelivery republishes the log.
125+
- Version conflicts return errors so redelivery reloads current state.
126+
- A different terminal request outcome is left unchanged.
127+
- `conclude_dlq` replays a referenced failure instead of creating another one.
128+
129+
To preserve in-flight messages, first deploy a decoder that accepts both legacy bare `BatchID` payloads and `ConcludeMessage`. Then migrate batch producers, add failure producers, and remove legacy decoding after the topic retention window.

0 commit comments

Comments
 (0)