Documentation
¶
Overview ¶
Package backoff computes successive retry delays with exponential growth, a bounded maximum, and random jitter.
Problem ¶
Exponential-backoff-with-jitter is easy to get subtly wrong: unbounded growth overflows the int64 nanosecond range into negative durations, a zero jitter ceiling panics the random source, and the arithmetic is duplicated (with diverging guards) across every package that retries or paces work.
Solution ¶
Schedule is a pure, per-call delay calculator: given a base delay, a growth factor, a jitter ceiling, and a maximum, each call to Schedule.Next returns the next delay and advances the progression. It performs no timing, scheduling, goroutines, or I/O — callers own their own timers and loops and simply ask the Schedule for the next duration.
AddJitter exposes the jitter step on its own for callers that pace work at a fixed interval rather than backing off exponentially.
Jitter strategies ¶
By default a Schedule adds a fixed random ceiling (JitterAdditive); its desynchronizing effect shrinks as the delay grows. JitterFull and JitterEqual instead scale the jitter with the delay, decorrelating concurrent clients far better at large delays. Select one via Config.Strategy.
Bounds and overflow ¶
Both the internal progression and the jitter addition are clamped so no delay can ever overflow into a negative duration, regardless of factor, attempt count, or configured maximum. The growth is capped well below the int64 limit (~146 years), and the jitter addition saturates at math.MaxInt64 rather than wrapping. Out-of-contract negative or NaN inputs are floored to zero rather than producing a negative delay.
Delays are computed in float64, so integer-nanosecond precision is exact only up to ~2^53 ns (~104 days); beyond that a result may differ by a few nanoseconds. This is immaterial at realistic (sub-minute) delays.
Usage ¶
s := backoff.New(backoff.Config{
Base: 100 * time.Millisecond,
Factor: 2,
Jitter: 50 * time.Millisecond,
MaxDelay: 30 * time.Second,
})
for {
// ... attempt work ...
time.Sleep(s.Next()) // 100ms, 200ms, 400ms, ... capped at 30s, each +[0,50ms)
}
Concurrency ¶
A Schedule is stateful and must not be used concurrently; construct one per retry sequence. AddJitter is a stateless function and is safe for concurrent use.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AddJitter ¶
AddJitter returns base plus a uniform random duration in [0, ceil).
A ceil <= 0 disables jitter and returns base unchanged. The addition is overflow-safe: if base+jitter would exceed math.MaxInt64 nanoseconds it saturates at math.MaxInt64 instead of wrapping. base is expected to be >= 0.
AddJitter is safe for concurrent use.
Types ¶
type Config ¶
type Config struct {
// Base is the pre-jitter delay returned by the first [Schedule.Next] call,
// before any growth is applied. Should be > 0.
Base time.Duration
// Factor multiplies the pre-jitter delay after each [Schedule.Next] call.
// Use >= 1 for non-decreasing backoff; 1 keeps the delay constant at Base.
Factor float64
// Jitter is the exclusive upper bound of the random duration added to each
// returned delay to desynchronize concurrent clients. <= 0 disables jitter.
// Only [JitterAdditive] uses this; the other strategies scale jitter with the
// delay and ignore it.
Jitter time.Duration
// MaxDelay caps the pre-jitter delay. <= 0 applies only the internal safety
// cap (maxSafeDelay).
MaxDelay time.Duration
// Strategy selects how jitter is derived from each delay (default
// [JitterAdditive]). See [JitterStrategy].
Strategy JitterStrategy
}
Config defines the parameters of an exponential backoff Schedule.
The zero value is not meaningful; construct a fully specified Config. New does not validate these (callers that retry typically validate via their own options): out-of-range values degrade gracefully rather than panic, as documented on New.
type JitterStrategy ¶
type JitterStrategy int
JitterStrategy selects how random jitter is derived for each backoff delay.
The additive strategy adds a fixed ceiling, whose desynchronizing effect shrinks as the delay grows; the full and equal strategies scale the jitter with the delay itself and therefore decorrelate concurrent clients far better at large delays (see the AWS "Exponential Backoff And Jitter" analysis).
const ( // JitterAdditive adds a fixed random duration in [0, Config.Jitter) on top of // the clamped exponential delay. It is the default. Because the ceiling is // fixed, its decorrelating effect shrinks as the delay grows. JitterAdditive JitterStrategy = iota // JitterFull replaces the delay with a uniform random value in [0, delay), // where delay is the clamped exponential value ("full jitter"). It // decorrelates retries best because the spread scales with the delay. // Config.Jitter is ignored. JitterFull // JitterEqual keeps half of the clamped exponential delay and randomizes the // rest, yielding a wait in [delay/2, delay) (integer division, so the floor is // floor(delay/2)). It trades some decorrelation for a guaranteed minimum wait. // Config.Jitter is ignored. JitterEqual )
func (JitterStrategy) Valid ¶
func (s JitterStrategy) Valid() bool
Valid reports whether s is a defined JitterStrategy.
type Schedule ¶
type Schedule struct {
// contains filtered or unexported fields
}
Schedule generates successive exponential-backoff delays with clamping and jitter. It is a pure per-call calculator that performs no timing, scheduling, or I/O.
A Schedule is stateful — each Schedule.Next advances the progression — and is therefore not safe for concurrent use. Create one per retry sequence.
Example ¶
package main
import (
"fmt"
"time"
"github.com/tecnickcom/nurago/pkg/backoff"
)
func main() {
s := backoff.New(backoff.Config{
Base: 100 * time.Millisecond,
Factor: 2,
Jitter: 0, // disabled so the example output is deterministic
MaxDelay: 350 * time.Millisecond,
})
for range 4 {
fmt.Println(s.Next())
}
}
Output: 100ms 200ms 350ms 350ms
func New ¶
New returns a Schedule for the given Config.
New never fails and never panics. Callers that need validation should perform it before constructing. Out-of-range inputs degrade gracefully: the pre-jitter delay is floored to the non-negative range [0, maxDelay], so a non-positive Base, a Factor < 1, and even a negative or NaN Factor can never yield a negative delay. The exact non-negative sequence for such inputs is unspecified (a negative Factor, for example, oscillates rather than decaying), but every value is non-negative and the growth stays bounded by maxSafeDelay.
func (*Schedule) Next ¶
Next returns the next backoff delay and advances the progression.
The pre-jitter delay is clamped to MaxDelay (or the internal safety cap when MaxDelay <= 0), jitter is applied according to the configured JitterStrategy, and the stored progression is multiplied by Factor and re-capped for the following call.