retry

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package retry provides retry policies for resilient operation execution.

The root package holds the Policy seam and the vocabulary for deciding what is worth retrying: ErrUnretryable, Unretryable, and IsTerminal. It also holds the vocabulary for reporting a loop that gave up — ErrExhausted, ExhaustedError, Exhausted, and Attempts — so a caller can tell "the database refused the connection" from "the database refused the connection five times over four seconds", which is the difference between a failed request and a request that spent its latency budget failing. Policies are constructed from retry/config, which owns Config, the exponential-backoff implementation with optional jitter, and the DelayFor schedule that callers who cannot retry by sleeping use to compute their own wake-up times.

The split runs that way — rather than the config subpackage wrapping a root constructor — because everything consuming a Config has to sit on the same side of the import edge as Config itself, and config subpackages in this module import their root, never the reverse.

Index

Constants

This section is empty.

Variables

View Source
var ErrExhausted = errors.New("retries exhausted")

ErrExhausted marks the error a policy returns when it has spent every attempt it was given without the operation succeeding.

It is a distinct answer from the operation's own last error, which it wraps: "the database refused the connection" and "the database refused the connection five times over four seconds" are different facts about a request, and only the second one explains where its latency went. Match it with errors.Is; read the count with Attempts.

View Source
var ErrUnretryable = errors.New("unretryable")

ErrUnretryable marks an error as one that Execute must not retry. Wrap a returned error with Unretryable (or return anything that wraps ErrUnretryable) to stop the retry loop immediately instead of exhausting the remaining attempts.

Functions

func Attempts

func Attempts(err error) (uint, bool)

Attempts reports how many attempts produced err, and whether err is the kind of error that knows.

Only an exhausted loop knows. An operation that failed unretryably, or one whose context was canceled mid-loop, returns the error it got — the loop stopped for a reason of its own rather than for want of attempts.

func DefaultRand

func DefaultRand() float64

DefaultRand is the source a strategy uses when none is given: math/rand/v2's global Float64, which needs no seeding.

It is not cryptographic and does not need to be. Jitter decorrelates the timing of retries between processes; nothing about it is a secret, and a caller who could predict the next wait gains nothing by it.

func Exhausted

func Exhausted(attempts uint, err error) error

Exhausted wraps err as the end of a loop that ran attempts times.

A nil err is nothing to report: a loop that succeeded did not exhaust itself, however many attempts it took.

func IsTerminal

func IsTerminal(ctx context.Context, err error) bool

IsTerminal reports whether an operation error should abort a retry loop rather than trigger another attempt: the loop's own context is done, so retrying can never succeed, or the error is explicitly non-retryable.

The question is asked of ctx, not of err. Matching err against context.DeadlineExceeded treats a *per-attempt* timeout as terminal — and a per-attempt timeout is the single most common transient failure there is, so the loop gave up on attempt one for exactly the case it exists to survive. An operation that bounds itself with its own context.WithTimeout hands back a wrapped DeadlineExceeded while this loop's deadline is nowhere near.

It is exported because the policies that consume it live in retry/config, and because a caller writing its own loop wants the same answer.

func None

func None(d time.Duration) time.Duration

None returns the delay unchanged.

It is what a UseJitter=false config selects, so that the presence of jitter is a choice of strategy rather than a branch around one.

func Unretryable

func Unretryable(err error) error

Unretryable wraps err so Execute stops retrying on it. The original error is preserved in the chain, so errors.Is/As against it still work.

Types

type ExhaustedError

type ExhaustedError struct {
	// Err is the error the final attempt returned.
	Err error
	// Attempts is how many times the operation ran, including the first.
	Attempts uint
}

ExhaustedError is what a policy returns when the attempts run out, carrying the count that produced it alongside the last error.

Callers rarely construct one — Exhausted does — but the fields are exported so a caller matching with errors.As can read the count without a helper.

func (*ExhaustedError) Error

func (e *ExhaustedError) Error() string

func (*ExhaustedError) Is

func (e *ExhaustedError) Is(target error) bool

Is reports ErrExhausted, so a caller can ask the question without naming the type.

func (*ExhaustedError) Unwrap

func (e *ExhaustedError) Unwrap() error

Unwrap returns the last error, so everything a caller could match against before the loop gave up still matches.

type Jitter

type Jitter func(d time.Duration) time.Duration

Jitter perturbs a computed backoff so that callers which failed together do not retry together.

The strategies below differ in how much of the delay they are willing to give up, and that difference is the whole decision: it trades how well a fleet spreads against how short a single wait may become. Naming them is what keeps the choice visible at the call site — the same perturbation written inline reads as arithmetic, and two call sites that meant different distributions look identical.

A strategy is expected to be monotone in nothing and to return a non-negative duration for a non-negative one; a zero or negative delay comes back unchanged, because there is nothing to spread.

func Equal

func Equal(r Rand) Jitter

Equal holds half the delay and spreads the other half, drawing from [d/2, d).

It is the right one when the caller waits in place, because it keeps a floor of half the schedule under every wait: a poller that backed off to ten seconds cannot draw a ten-millisecond one and turn back into a hot loop. A fleet still spreads, just across half the window rather than all of it.

It never exceeds d, which is load-bearing wherever the un-jittered interval is itself a promise — a refresh scheduled inside a TTL, a renewal inside a lease.

A delay too small to halve comes back unchanged. Perturbing a one-nanosecond wait buys nothing and would have to break the floor this strategy is named for to do it.

func Full

func Full(r Rand) Jitter

Full spreads a delay across the whole interval, drawing uniformly from [0, d).

It is the strongest spread available and the right one when many processes write their next attempt somewhere durable and then stop thinking about it: a fleet that all failed on the same contended row wants its next attempts scattered over the entire window, because anything less leaves a shoulder they will re-collide on.

The cost is that a single wait can land arbitrarily close to zero, which for a caller that sleeps in place is a hot loop. Such callers want Equal, or a Full floored by AtLeast.

func (Jitter) AtLeast

func (j Jitter) AtLeast(minimum time.Duration) Jitter

AtLeast floors a strategy's output.

Full can draw a delay arbitrarily close to zero, and for a caller that persists "try again at T" that means a row which becomes claimable immediately and spins against whatever failure produced it rather than waiting the failure out. The floor is what makes the strongest spread safe for a caller that cannot sleep.

type Policy

type Policy interface {
	Execute(ctx context.Context, operation func(ctx context.Context) error) error
}

Policy executes operations with retry logic.

type Rand

type Rand func() float64

Rand draws a value in [0, 1). math/rand/v2's Float64 satisfies it and is what DefaultRand is.

It is a parameter rather than a package-level draw so that a caller who needs a schedule to be reproducible — a test asserting an exact wait, a simulation replaying a fleet — can supply one, and so that nothing here reaches for a global source a caller cannot see.

Directories

Path Synopsis
Package retrycfg builds a retry.Policy from configuration — exponential backoff, which an unset provider selects, or noop, which has to be named.
Package retrycfg builds a retry.Policy from configuration — exponential backoff, which an unset provider selects, or noop, which has to be named.
Package noop is the retry.Policy that does not retry: Execute calls the operation once and returns whatever the operation returned.
Package noop is the retry.Policy that does not retry: Execute calls the operation once and returns whatever the operation returned.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL