Documentation
¶
Overview ¶
Package retry adds durability and tail-latency control to calls over unreliable transports (node-to-node RPCs, S3). It provides three things, all allocation-light and used only off the data plane's hot path:
- bounded retries with exponential backoff + jitter (Do), for calls that are safe to repeat;
- per-attempt timeouts, so a single slow attempt cannot stall a call for the whole deadline;
- hedged (opportunistic concurrent) retries (Hedge) for idempotent calls: stagger a second attempt once the first is slow, race them, take the first success, and cancel the losers — the classic defense against a tail of slow/stuck requests in a lossy, noisy environment.
A Policy is a value; the zero value runs exactly one attempt with no timeout (i.e. a plain call), so wiring it in is always safe.
Index ¶
- func ConnFailure(err error) bool
- func Do[T any](ctx context.Context, p Policy, fn func(context.Context) (T, error)) (T, error)
- func Hedge[T any](ctx context.Context, p Policy, thunks []func(context.Context) (T, error)) (T, error)
- func Repeat[T any](thunk func(context.Context) (T, error), n int) []func(context.Context) (T, error)
- func RetryableStatus(code int) bool
- func Transient(err error) bool
- type Policy
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ConnFailure ¶
ConnFailure reports whether err means the request never reached the server — a dial failure or a refused connection. It is the *safe* retry predicate for non-idempotent calls (writes): retrying is correct only when we know the server did not act on the request. An ambiguous failure (a timeout after the body was sent) returns false, giving writes at-most-once semantics.
func Do ¶
Do runs fn with sequential retries under the policy: at most Policy.MaxAttempts attempts, each bounded by Policy.PerTryTimeout, spaced by exponential backoff. It stops early on success, on a non-retryable error, or when ctx is done. Use it for calls that are safe to repeat but should not run concurrently (e.g. a write with a bounded retry budget).
func Hedge ¶
func Hedge[T any](ctx context.Context, p Policy, thunks []func(context.Context) (T, error)) (T, error)
Hedge issues the thunks as opportunistic concurrent attempts: it launches the first immediately, and launches each next one either after Policy.HedgeDelay elapses with no result (the in-flight attempt is slow) or as soon as an attempt fails retryably (failover). It returns the first success and cancels the rest; a non-retryable error short-circuits. All thunks must be idempotent — several may run at once. With one thunk it degrades to a single Policy.PerTryTimeout-bounded call.
func Repeat ¶
func Repeat[T any](thunk func(context.Context) (T, error), n int) []func(context.Context) (T, error)
Repeat builds n identical thunks from one call, for hedging against a single endpoint (e.g. a slow S3 GET re-issued on a fresh connection): the second attempt is a retry, not a different host.
func RetryableStatus ¶
RetryableStatus reports whether an HTTP status code is a transient server-side failure (5xx, or 429 Too Many Requests). 4xx (except 429) is a permanent client error and must not be retried.
func Transient ¶
Transient reports whether err is worth retrying for an *idempotent* call: transport failures (dial/connection errors, resets, timeouts), a per-attempt deadline, and unexpected EOFs. A parent-context cancellation is never retried (the caller gave up). Unknown non-nil errors are treated as transient, since an idempotent call is safe to repeat — pair this only with calls that truly are idempotent.
Types ¶
type Policy ¶
type Policy struct {
// MaxAttempts is the total number of attempts for one logical call (≥1). 0 means 1.
MaxAttempts int
// PerTryTimeout bounds a single attempt. 0 disables it (the attempt runs until the parent
// context's deadline). With hedging, a slow attempt is bypassed by [HedgeDelay] regardless;
// the per-try timeout additionally bounds the resources a stuck attempt holds.
PerTryTimeout time.Duration
// BaseBackoff is the wait before the second sequential attempt in [Do]; it doubles each attempt
// up to MaxBackoff, with equal jitter. 0 disables backoff (attempts run back-to-back).
BaseBackoff time.Duration
MaxBackoff time.Duration
// HedgeDelay is how long [Hedge] waits for the in-flight attempt before launching the next one
// concurrently. 0 makes [Hedge] a pure sequential failover (the next launches only when the
// current one fails). Tune it above the normal-case latency so the fast path never hedges.
HedgeDelay time.Duration
// Retryable classifies an error: true ⇒ try the next attempt, false ⇒ fail immediately (e.g.
// a "not found" or a 4xx is permanent). nil ⇒ retry every error except a parent-context
// cancellation.
Retryable func(error) bool
// Rand returns a float in [0,1) for backoff jitter. nil ⇒ the default global source.
Rand func() float64
// Observability hooks (all optional). attempt is 0-based; attempt 0 is the first try.
OnAttempt func(attempt int) // every launch (incl. the first)
OnRetry func(attempt int, err error, wait time.Duration) // before a sequential retry waits
OnHedge func(attempt int) // when a hedged attempt is launched (attempt ≥ 1)
}
Policy configures retries, per-attempt timeouts, and hedging. It is a plain value; copy freely.