Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
3.3 KiB
Markdown
72 lines
3.3 KiB
Markdown
# Retry and Ambiguity
|
|
|
|
The platform never decides a retry from the HTTP method alone (design D-09). A second attempt
|
|
happens only when idempotency, body replayability, execution evidence, deadline, and retry budget
|
|
all permit it.
|
|
|
|
## Execution evidence
|
|
|
|
| Evidence | Meaning | Typical cause |
|
|
|---|---|---|
|
|
| `NOT_SENT` | Proven that the server never received the request | profile rejection, pool timeout, DNS failure, connect failure, pre-request TLS failure, HTTP/2 `REFUSED_STREAM` |
|
|
| `SENT_NO_RESPONSE` | Some or all of the request was written, no final header arrived | partial write, response-header timeout, connection reset |
|
|
| `RESPONSE_RECEIVED` | Final headers arrived, whatever the status | 2xx, 4xx, 5xx, redirect |
|
|
| `PARTIAL_RESPONSE` | Headers and part of the body arrived | reset during decode, interrupted stream |
|
|
|
|
`NOT_SENT` is only produced by a stage failure that proves it. A generic engine I/O error is never
|
|
upgraded to `NOT_SENT`, because that is exactly how a timeout becomes a duplicate payment.
|
|
|
|
## Body replayability
|
|
|
|
| Body | Replayability |
|
|
|---|---|
|
|
| immutable `byte[]` | `REPLAYABLE` |
|
|
| DTO plus a deterministic codec | `REPLAYABLE` |
|
|
| reopenable file or resource supplier | `REOPENABLE` |
|
|
| a single `InputStream` instance | `ONE_SHOT` |
|
|
| publisher factory | as declared |
|
|
| publisher instance | `ONE_SHOT` |
|
|
| multipart | the weakest part |
|
|
|
|
## Decision order
|
|
|
|
`DefaultRetryEligibilityEngine` evaluates in this order, and a later rule can never re-enable
|
|
something an earlier one forbade:
|
|
|
|
1. attempts exhausted → `RetryDenied.maxAttempts()`
|
|
2. retry budget empty → `RetryDenied.budgetExhausted()`
|
|
3. body not replayable → `RetryDenied.bodyNotReplayable()`
|
|
4. first byte already delivered → `RetryDenied.responseAlreadyDelivered()`
|
|
5. runtime draining → `RetryDenied.runtimeDraining()`
|
|
6. remaining deadline below the minimum attempt budget → `RetryDenied.deadline()`
|
|
7. permanent failure category → `RetryDenied.permanentFailure(...)`
|
|
8. `SENT_NO_RESPONSE` on an operation that is not safely idempotent → `AmbiguousFailure`
|
|
9. status- and failure-specific rules
|
|
|
|
## Status rules
|
|
|
|
| Status | Decision |
|
|
|---|---|
|
|
| 408 | retry inside deadline and budget |
|
|
| 425 | at most one retry, first attempt only |
|
|
| 429 | retry inside `Retry-After`, deadline, and budget |
|
|
| 401 | one refresh-and-replay, safe replayable operations only |
|
|
| 500 | denied unless the upstream registered it as transient **and** the operation is safely idempotent |
|
|
| 502, 503, 504 | retry for safely idempotent operations; ambiguous otherwise |
|
|
| other 4xx | denied |
|
|
|
|
## Ambiguity
|
|
|
|
A non-idempotent request that reached `SENT_NO_RESPONSE` raises
|
|
`HttpAmbiguousExecutionException`. It is a third answer on purpose: retrying may duplicate a side
|
|
effect, and reporting a plain failure would tell the caller the request did not happen, which may
|
|
be false. The caller reconciles, usually by querying the upstream or replaying with an idempotency
|
|
key.
|
|
|
|
## Budget and backoff
|
|
|
|
Retry tokens come from a per-upstream token bucket sized as a fraction of real traffic, so a failing
|
|
upstream cannot be flooded by retries from a healthy fleet. Backoff is exponential with full or
|
|
decorrelated jitter, bounded by `max-backoff`, by `Retry-After`, and by the remaining deadline. No
|
|
connection and no bulkhead permit is held while a backoff is waiting.
|