51 lines
2.8 KiB
Markdown
51 lines
2.8 KiB
Markdown
# ADR-GRPC-003: Transport, business and stream evidence are three axes, and none implies another
|
|
|
|
- Status: accepted
|
|
- Date: 2026-08-30
|
|
- Scope: `:grpc:grpc-core-api`, `:grpc:grpc-policy`, `:grpc:grpc-testkit`
|
|
|
|
## Context
|
|
|
|
A failed RPC produces a status code, and a status code is not an answer to the question the caller
|
|
actually has. `DEADLINE_EXCEEDED` on a mutation does not say whether the mutation happened;
|
|
`UNAVAILABLE` after the request was sent does not say the server never saw it; response headers
|
|
arriving does not say a transaction committed.
|
|
|
|
Every one of those is a place where a plausible inference produces a duplicate write or a lost one,
|
|
and none of them is visible in a test that only exercises the happy path.
|
|
|
|
## Decision
|
|
|
|
Model what happened as three independent axes, and refuse the inferences between them.
|
|
|
|
`GrpcTransportEvidence` records what the client observed on the wire, and distinguishes `NOT_SENT` —
|
|
the client watched its own send fail — from `UNOBSERVED`, which is every other case where nothing is
|
|
known. `GrpcBusinessEvidence` records what the application confirmed, with `COMMIT_UNKNOWN` as a real
|
|
state rather than a placeholder. `GrpcStreamEvidence` is a sealed hierarchy whose non-empty cases all
|
|
carry a position, because "partial" without a last sequence can be neither resumed nor reconciled.
|
|
|
|
`GrpcExecutionEvidence` holds all three and rejects combinations nobody could have observed: a unary
|
|
call with stream evidence, or a request the client watched fail to send that nonetheless carries
|
|
business evidence. Promoting response headers to a confirmed commit is possible only by editing
|
|
`withResponseHeadersSeen`, which is one method rather than a plausible line in an interceptor.
|
|
|
|
`GrpcCompletionOutcome.forMutation` derives what a caller may conclude, and defaults
|
|
`DEADLINE_EXCEEDED` and post-send `UNAVAILABLE` on a mutation to `COMPLETION_UNKNOWN`.
|
|
|
|
The same types are used by the failure model and by the observation convention, so an incident has
|
|
one account of a call rather than two.
|
|
|
|
## Consequences
|
|
|
|
**A whole class of retry bug becomes unrepresentable.** `GrpcRetryEligibility` reads all three axes
|
|
plus the idempotency profile; a caller cannot reach "retry" from a status alone because the status
|
|
alone is not an input.
|
|
|
|
**The fault lane has something to check.** `GrpcTransportEvidenceClassifier` turns a client's
|
|
observations into evidence and refuses to infer `NOT_SENT` from an unobserved state — and the lane
|
|
exercises it against a real connection dropped mid-call, not against a mock.
|
|
|
|
**Callers must handle a third outcome.** `COMPLETION_UNKNOWN` is not a failure and not a success, and
|
|
a caller that treats it as either is wrong. `GrpcOperationStatusQuery` and `GrpcCompletionReconciler`
|
|
exist so that resolving it is a supported path rather than an exercise for the caller.
|