-
Notifications
You must be signed in to change notification settings - Fork 3
docs: History API RFC #325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+163
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| # Gateway History APIs | ||
|
|
||
| Design notes for gateway history APIs that return retained lifecycle events selected by SubmitQueue request ID or change ID. | ||
|
|
||
| This document captures **design decisions and rationale only**. | ||
|
|
||
| ## Problem | ||
|
|
||
| Users need to inspect how a request progressed through SubmitQueue, not only its current reconciled state. They may start with the `sqid` returned by `Land` or with a provider-specific change ID represented by a change URI supplied to `Land`. The existing `Status` API collapses the append-only request log into one current status, which is appropriate for polling but hides the sequence of events needed for debugging and lifecycle displays. | ||
|
|
||
| The gateway owns the request log and is the only service that reads it. The history APIs preserve that ownership boundary by serving gateway-owned `RequestLog` records for the request or requests selected by the caller. | ||
|
|
||
| ## API Shape | ||
|
|
||
| The gateway exposes two read-only RPCs because an `sqid` selects one event list while a change ID may select multiple requests: | ||
|
|
||
| ```proto | ||
| message HistoryBySQIDRequest { | ||
| // Globally unique identifier for a request, as returned by Land. | ||
| string sqid = 1; | ||
| } | ||
|
|
||
| message HistoryEvent { | ||
| // Time the request-log entry was created, in milliseconds since Unix epoch. | ||
| int64 timestamp_ms = 1; | ||
| // Customer-friendly request status recorded by the event. | ||
| string status = 2; | ||
| // Error message associated with the event. Empty if none. | ||
| string last_error = 3; | ||
| // Free-form display or debugging context associated with the event. | ||
| map<string, string> metadata = 4; | ||
| } | ||
|
|
||
| message HistoryBySQIDResponse { | ||
|
behinddwalls marked this conversation as resolved.
|
||
| // Retained request-log events ordered by timestamp_ms ascending with a stable tie-breaker. | ||
| repeated HistoryEvent events = 1; | ||
|
albertywu marked this conversation as resolved.
|
||
| } | ||
|
|
||
| message HistoryByChangeIDRequest { | ||
| // Provider-specific change identifier represented by a change URI supplied to Land. | ||
| string change_id = 1; | ||
| } | ||
|
|
||
| message RequestHistory { | ||
| // Globally unique identifier for the request associated with these events. | ||
| string sqid = 1; | ||
| // Retained request-log events ordered by timestamp_ms ascending with a stable tie-breaker. | ||
| repeated HistoryEvent events = 2; | ||
| } | ||
|
|
||
| message HistoryByChangeIDResponse { | ||
| // Request histories ordered by the numeric sqid counter ascending. | ||
| repeated RequestHistory histories = 1; | ||
| } | ||
|
|
||
| service SubmitQueueGateway { | ||
| rpc HistoryBySQID(HistoryBySQIDRequest) returns (HistoryBySQIDResponse) {} | ||
| rpc HistoryByChangeID(HistoryByChangeIDRequest) returns (HistoryByChangeIDResponse) {} | ||
| } | ||
| ``` | ||
|
|
||
| `HistoryBySQID` returns one list of events for exactly one request. `HistoryByChangeID` returns a list of request histories because the same change can be submitted more than once. Each history includes its `sqid` so callers can distinguish submissions and use the identifier with other gateway APIs. | ||
|
|
||
| ## Status Contract | ||
|
|
||
| `HistoryEvent.status` is a string, not a protobuf enum. Its value is populated from `entity.RequestStatus`, the same customer-facing status type stored in `RequestLog` and returned by `Status`. | ||
|
|
||
| Keeping the wire field as a string allows SubmitQueue to add request statuses without requiring clients to adopt a new generated enum before they can read the response. Clients must tolerate status strings they do not recognize. | ||
|
|
||
| ## Event Projection | ||
|
|
||
| The history APIs do not introduce a projection table or persist a second history model. Their controllers read retained rows directly from `RequestLogStore` and map each stored `RequestLog` row to exactly one `HistoryEvent` in the protobuf response: | ||
|
|
||
| | `RequestLog` field | `HistoryEvent` field | | ||
| | --- | --- | | ||
| | `TimestampMs` | `timestamp_ms` | | ||
| | `Status` | `status` | | ||
| | `LastError` | `last_error` | | ||
| | `Metadata` | `metadata` | | ||
|
|
||
| `RequestVersion` is intentionally excluded. It is an internal reconciliation signal used to determine the current state and is not part of the public lifecycle event contract. | ||
|
|
||
| `RequestID` is excluded from each event because the request is identified by the `HistoryBySQID` request or by `RequestHistory.sqid`. | ||
|
|
||
| ## Ordering | ||
|
|
||
| Events are returned in `timestamp_ms` ascending order so the response reads as a lifecycle from oldest to newest. This matches the ordering contract of `RequestLogStore.List`. | ||
|
|
||
| Request-log timestamps are generated by callers, not by the storage backend. The API therefore defines chronological order by the recorded `timestamp_ms`, not database insertion order. | ||
|
|
||
| Multiple events may have the same timestamp. Events with equal timestamps are ordered by a stable, implementation-defined tie-breaker so repeated reads of the same retained rows return the same sequence. The tie-breaker is not exposed in the API because it has no lifecycle meaning. For example, the MySQL implementation uses the persisted `salt` column as its secondary sort key. | ||
|
|
||
| `HistoryByChangeIDResponse.histories` is ordered by the numeric SQID counter ascending. Implementations must parse the counter rather than compare SQIDs lexicographically, so `main/2` precedes `main/10`. | ||
|
|
||
| ## Preserve Every Stored Event | ||
|
|
||
| Both history RPCs return one event for every retained request-log row. They do not deduplicate, coalesce adjacent statuses, reconcile events into a current state, or otherwise rewrite the sequence. | ||
|
|
||
| Repeated or equivalent entries can be produced by retries. For example, a producer or consumer may retry after an uncertain failure, and DLQ reconciliation may append a replacement entry when it cannot confirm that an earlier insert succeeded. `RequestLogStore.Insert` is append-only and does not provide an idempotency key. | ||
|
|
||
| These repeated entries are part of the stored audit trail and must remain visible. Hiding them would require an arbitrary definition of event equivalence and could conceal useful retry behavior during debugging. | ||
|
|
||
| ## Pagination | ||
|
|
||
| The initial RPCs return all currently retained events matching the selected identifier in one response and do not support pagination. | ||
|
|
||
| A SubmitQueue request has a bounded lifecycle under normal operation, and a change is expected to have a modest number of retained submissions, so the additional cursor and query complexity is not justified initially. Pagination can be added later if production histories demonstrate that response size needs an explicit bound. | ||
|
|
||
| ## Consistency | ||
|
albertywu marked this conversation as resolved.
|
||
|
|
||
| The history APIs are eventually consistent. Direct request-log writes become visible after storage persistence, while events sent through the log topic become visible after the gateway consumes and persists them. A successful response may therefore briefly omit recently emitted events. | ||
|
|
||
| Unlike `Status`, the history APIs do not reconcile competing log entries. They expose retained event sequences directly. | ||
|
|
||
| ## Errors | ||
|
|
||
| Error behavior follows the conventions established by `Status`: | ||
|
|
||
| - An empty `sqid` passed to `HistoryBySQID` is an invalid request. | ||
| - An empty `change_id` passed to `HistoryByChangeID` is an invalid request. | ||
| - If no request-log records exist for an `sqid`, `HistoryBySQID` returns the existing `RequestNotFoundError`. | ||
| - If no retained request histories match a `change_id`, `HistoryByChangeID` returns a change-ID-specific not-found user error. | ||
| - A request-log storage failure is returned as an infrastructure error. | ||
|
|
||
| Using the existing request not-found error for `sqid` lookups keeps point lookups consistent across the gateway API. | ||
|
|
||
| ## Flow | ||
|
|
||
| ```text | ||
| HistoryBySQIDRequest(sqid) | ||
| | | ||
| v | ||
| validate sqid | ||
| | | ||
| v | ||
| RequestLogStore.List(sqid) | ||
| | | ||
| v | ||
| project each RequestLog to one HistoryEvent | ||
| | | ||
| v | ||
| HistoryBySQIDResponse(events) | ||
|
|
||
| HistoryByChangeIDRequest(change_id) | ||
| | | ||
| v | ||
| validate change_id | ||
| | | ||
| v | ||
| resolve matching sqids | ||
| | | ||
| v | ||
| RequestLogStore.List(sqid) for each match | ||
| | | ||
| v | ||
| project each RequestLog to one HistoryEvent | ||
| | | ||
| v | ||
| HistoryByChangeIDResponse(histories) | ||
| ``` | ||
|
|
||
| The API contract does not prescribe how a change ID is mapped to matching requests. That lookup is an implementation concern and must preserve the gateway's ownership of the history read path. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.