Documentation
¶
Overview ¶
Package backoff computes an exponential backoff: how long a loop that keeps failing waits before its next attempt. It is a kernel primitive — stdlib-only and domain-neutral: a duration, a growth factor, a ceiling and a jitter, with no Retry, no Job and no Delivery in a signature.
One curve serves every loop that backs off. The retry policy waits it between attempts; a supervised goroutine waits it between restarts, an outbox between deliveries, a state machine between failed transitions, a server between failed accepts. Each of those used to compute its own copy, and the copy that lived in the retry policy carried a defect the shared one fixes (ADR 0103): see Value for the float-to-duration conversion that made amd64 retries stop waiting at attempt 35.
The curve is two claims kept apart on purpose. Grow is the deterministic growth, a PURE function of the attempt; Widen is the randomisation laid on top of it. Value.Delay composes the two. A suite that cannot assert the growth without the jitter can assert neither precisely, and a caller that wants the deterministic curve asks for it by name.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Grow ¶
Grow returns the deterministic half of the curve: base × multiplier^(attempt−1), held at ceiling when one is set (a zero ceiling is none) and at the longest time.Duration otherwise. It is a PURE function of its arguments.
multiplier is normalised as NormalMultiplier does, and attempt is read as at least 1, so no input makes it return a negative duration or loop past the few dozen multiplications that reach the bound.
func NormalJitter ¶
NormalJitter clamps a jitter fraction into [0, 1], reading NaN as none.
NaN is resolved FIRST because Go's min and max propagate it: min(max(NaN, 0), 1) is NaN, and NaN <= 0 is false, so it would sail past every later guard into a float-to-integer conversion Go leaves implementation-defined.
func NormalMultiplier ¶
NormalMultiplier resolves a growth factor that would not grow — 1 or below, or NaN — to the doubling default, and returns any other factor unchanged, +Inf included: Grow holds an infinite growth at the ceiling.
func Widen ¶
Widen returns delay widened by a uniform random fraction of itself, drawn from [0, jitter) and ADDED, so the average wait keeps growing with the attempt rather than being pulled back toward zero.
jitter is normalised as NormalJitter does; zero hands delay back unchanged, and so does a delay that is zero, negative or too small to split. The widened wait stays representable: it is spread across what is left below the longest time.Duration rather than past it.
Types ¶
type Value ¶
type Value struct {
// BaseDelay is the wait after the first failure. Zero or negative waits
// nothing.
BaseDelay time.Duration
// MaxDelay is the ceiling on the grown delay. Zero means no ceiling.
MaxDelay time.Duration
// Multiplier is the growth factor between consecutive waits. 1 or below,
// and NaN, mean 2.
Multiplier float64
// Jitter widens each wait by a uniform random fraction of itself, drawn
// from [0, Jitter) and ADDED, after the MaxDelay ceiling — so when a
// ceiling is set a wait may reach MaxDelay × (1 + Jitter). Zero is the
// deterministic backoff.
//
// It exists because a deterministic backoff SYNCHRONISES the very callers
// it is meant to spread: N clients that fail against one dependency at the
// same instant retry at the same instant, N times over. Jitter turns those
// bursts back into a distribution.
Jitter float64
}
Value is an exponential backoff: the wait after the n-th consecutive failure is BaseDelay × Multiplier^(n−1), held at MaxDelay when one is set and widened by Jitter when one is asked for.
The zero value is usable and waits nothing: a zero BaseDelay retries at once. Every other field is normalised on each call, so no value can make Delay misbehave:
- a Multiplier of 1 or below, or NaN, grows by 2 — a flat or shrinking "exponential" backoff is a fixed-interval hammer on a dependency that is already struggling;
- a zero MaxDelay is no ceiling, and the growth then stops at the longest time.Duration rather than wrapping negative;
- a Jitter outside [0, 1], or NaN, is clamped into it.
The growth is compared against its bound in the float domain BEFORE any conversion. The copy this replaces converted first: past 2^63 nanoseconds Go leaves that conversion implementation-defined — math.MinInt64 on amd64, math.MaxInt64 on arm64 — and a negative wait is below every ceiling, so a one-second retry on amd64 stopped backing off at attempt 35.
func (Value) Delay ¶
Delay returns the wait after the attempt-th consecutive failure: Grow for the curve, then Widen for the jitter. attempt counts from 1; a smaller value is read as 1, so a loop that has not failed yet and asks anyway waits BaseDelay rather than nothing.
It never returns a negative duration, and it costs at most as many multiplications as it takes the delay to reach its ceiling, however large attempt is.