Documentation
¶
Overview ¶
Package retry runs an operation again on failure, with exponential backoff, optional jitter, a bounded attempt budget, and an early stop on terminal errors.
Do is the universal entry point: it wraps any context-aware function — an HTTP call, a SQL transaction, a broker publish, a queue job — so retry policy is composed at the call site instead of being reimplemented in each subsystem. Backoff is the delay/budget policy and is shared across the SDK (the HTTP client middleware, reliable workers, this package), so there is one place that decides how retries are spaced.
Usage ¶
err := retry.Do(ctx, retry.Config{
Backoff: retry.Backoff{
Base: 100 * time.Millisecond, Factor: 2, Max: 2 * time.Second, MaxAttempts: 4,
},
IsTerminal: func(err error) bool { return errors.Is(err, ErrBadInput) },
}, func(ctx context.Context) error {
return callFlakyService(ctx)
})
Semantics ¶
Do calls fn, and on a non-nil error waits Backoff.Next(attempt) and tries again, until fn succeeds, the attempt budget is spent, IsTerminal matches, or ctx is cancelled. It returns nil on the first success or the last error otherwise. Retries run in the calling goroutine and block on the backoff delay, so keep the budget and delays modest when the caller holds a resource for the duration (for example a queue lease, which another consumer may reclaim if the total retry time exceeds the lease timeout).
Config ¶
- Backoff: inter-attempt delay (Base/Factor/Max/Jitter) and the attempt budget (MaxAttempts). MaxAttempts <= 1 means a single attempt (no retry); Do never loops unboundedly even when MaxAttempts is 0.
- Strategy: override the delay cadence per attempt (e.g. Fixed for a constant delay, or any custom schedule); the budget still comes from Backoff.MaxAttempts.
- IsTerminal: classify an error as permanent to stop early (nil = every error is retryable until the budget is spent; return true unconditionally to stop on any error).
- OnRetry: observe each retry (attempt number + error) for logging or statistics; called only when a retry will actually happen.
- Sleep, Rand: seams overridden in tests for deterministic timing and jitter; both default to real implementations.
See the package README for worked examples of each case.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Do ¶
Do calls fn and retries it on error according to cfg, until fn succeeds, the attempt budget is spent, cfg.IsTerminal matches, or ctx is cancelled. It returns nil on the first success, or the last error otherwise.
Do wraps any context-aware operation — an HTTP call, a DB transaction, a broker publish, a queue job — so retry policy is composed at the call site and not baked into each subsystem. Because retries run in the calling goroutine (blocking on the delay), keep the budget and delays modest when the caller holds a resource for the duration (for example a queue lease, which another consumer may reclaim if the total retry time exceeds the lease timeout).
Types ¶
type Backoff ¶
type Backoff struct {
// Base is the delay for the first retry (attempt 1).
Base time.Duration
// Max caps the delay (0 = uncapped). It is a hard ceiling on the final
// value: the returned delay never exceeds Max, even after jitter.
Max time.Duration
// Factor is the multiplier per attempt (defaults to 2 when <= 1).
Factor float64
// MaxAttempts is the retry budget; Exhausted reports when it is spent.
MaxAttempts int
// Jitter in [0,1] randomizes the delay by ±(jitter*delay). The caller
// supplies the random fraction to NextWithRand to keep Backoff deterministic
// and testable.
Jitter float64
}
Backoff computes retry delays with exponential growth and optional jitter, capped at Max, and exposes a max-attempts budget. It is shared by retrying HTTP clients, reliable processors, and the Do helper so backoff policy lives in one place.
func (Backoff) Exhausted ¶
Exhausted reports whether attempts has reached the MaxAttempts budget. A non-positive MaxAttempts means "never exhausted".
type Config ¶
type Config struct {
// Backoff sets the default delay schedule and — always — the attempt budget
// via its MaxAttempts field. MaxAttempts <= 1 means a single attempt (no
// retry); set it to >= 2 to retry. Do never loops unboundedly even when
// MaxAttempts is 0.
Backoff Backoff
// Strategy, when set, overrides the delay before each retry: it is called
// with the 1-based number of the attempt that just failed and returns the
// wait before the next attempt. Use it for a fixed delay (see Fixed) or any
// custom cadence. The attempt budget still comes from Backoff.MaxAttempts.
Strategy func(attempt int) time.Duration
// IsTerminal, when it returns true for an error, stops retrying and returns
// that error at once (a critical/permanent error). nil means every error is
// retryable until the budget is spent; return true unconditionally to stop on
// any error.
IsTerminal func(err error) bool
// OnRetry, when set, is called after an attempt fails and before Do waits to
// retry — once per retry that actually happens. It is not called for a
// success, a terminal error, or the final failed attempt (no retry follows
// those). Use it for logging or retry statistics. attempt is the 1-based
// number of the attempt that just failed.
OnRetry func(attempt int, err error)
// Sleep waits d or returns early with ctx.Err() if ctx is cancelled first.
// Defaults to a context-aware timer; override it in tests for determinism.
Sleep func(ctx context.Context, d time.Duration) error
// Rand returns the jitter fraction in [0,1) for each delay. Defaults to
// math/rand/v2; override it in tests. Consulted only by the default Backoff
// cadence (a custom Strategy computes its own delay).
Rand func() float64
}
Config tunes Do: how long to wait between attempts, how many attempts to make, which errors stop early, what to observe on each retry, and (for tests) how to sleep and how to draw jitter.