Documentation
¶
Overview ¶
Package retry provides function execution with retry, exponential backoff, and random jitter: Backoff runs a call until it succeeds, its attempt budget is spent, or its context ends, sleeping between attempts with exponentially growing, jittered, hard-capped delays.
The delay before retry i (counting from 1) starts at Policy.BaseDelay and doubles each retry, capped at Policy.MaxDelay; Policy.Jitter then spreads it uniformly over ±Jitter of its value (still never above MaxDelay), so simultaneous failers do not retry in lockstep. Backoff is a pure function with no shared state — it owns no goroutines and is safe for concurrent use. Design decisions are recorded in ADR-0011.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Backoff ¶
Backoff runs fn under policy until it returns nil, the attempt budget is spent, or ctx ends. It returns nil on the first success; the last error fn returned, verbatim, when MaxAttempts calls all failed (wrap fn to classify errors — Backoff never inspects them); or ctx.Err() when ctx ends before the first call or during a between-attempt sleep. fn receives ctx unchanged. A nil fn or an invalid policy panics — both are programming errors, not runtime conditions.
Example ¶
Backoff re-runs a call until it succeeds or its attempt budget is spent.
package main
import (
"context"
"errors"
"fmt"
"github.com/danielPoloWork/egl-utils-go/v2/pkg/retry"
)
func main() {
// A zero BaseDelay means retries are immediate, which is what keeps this
// example instant and clock-free. A real policy spreads load instead:
//
// retry.Policy{MaxAttempts: 5, BaseDelay: 100 * time.Millisecond,
// MaxDelay: 2 * time.Second, Jitter: 0.2}
//
// there the delay doubles per retry, stops at MaxDelay, and jitter keeps
// simultaneous failers from retrying in lockstep.
policy := retry.Policy{MaxAttempts: 3}
attempts := 0
err := retry.Backoff(context.Background(), policy, func(context.Context) error {
attempts++
if attempts < 3 {
return errors.New("upstream not ready")
}
return nil
})
fmt.Println(attempts, err == nil)
}
Output: 3 true
Example (Exhausted) ¶
The budget counts the first call, and a spent budget returns the last error the call produced.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/danielPoloWork/egl-utils-go/v2/pkg/retry"
)
func main() {
// MaxAttempts 2 means one call and one retry.
policy := retry.Policy{MaxAttempts: 2, BaseDelay: 0, MaxDelay: time.Second}
errBadRequest := errors.New("bad request")
calls := 0
err := retry.Backoff(context.Background(), policy, func(context.Context) error {
calls++
return errBadRequest
})
// Backoff retries anything non-nil, so a permanently-failing call still
// spends the whole budget: classify errors in fn and return nil-or-fail
// deliberately if some failures should not be retried.
fmt.Println(calls, errors.Is(err, errBadRequest))
}
Output: 2 true
Types ¶
type Policy ¶
type Policy struct {
// MaxAttempts is the total number of times fn may run, counting the
// first call: 1 means no retry. Must be > 0.
MaxAttempts int
// BaseDelay is the pre-jitter delay before the first retry; each further
// retry doubles it. Zero means immediate retries. Must be >= 0.
BaseDelay time.Duration
// MaxDelay is the hard cap: no sleep, jittered or not, ever exceeds it.
// Must be >= BaseDelay.
MaxDelay time.Duration
// Jitter is the fraction of each delay used as the jitter half-range: a
// delay d becomes uniform in [d*(1-Jitter), d*(1+Jitter)], then is
// re-capped at MaxDelay. Zero disables jitter. Must be in [0, 1].
Jitter float64
// contains filtered or unexported fields
}
Policy configures Backoff. The zero value is not usable: MaxAttempts must be at least 1.