Documentation
¶
Overview ¶
Package retry provides a shared exponential-backoff helper used by retry sites that previously open-coded the same loop with slightly different shapes. Today's call-sites:
- internal/database/connection.go::createConnectionPoolWithRetry bounds Lambda cold-start RDS Ping retries with a per-attempt deadline so a single hung TCP SYN doesn't burn the full retry budget.
- providers/gcp/services/computeengine/client.go::CreateCommitment retries on RESOURCE_EXHAUSTED with a fixed 1s/2s/4s sequence.
The AWS rate limiter at providers/aws/recommendations/ratelimiter.go is intentionally NOT migrated to this helper — its Wait/ShouldRetry/ Reset state-machine is structurally incompatible with the closure- based Do shape and rewriting its callers would be a separate change.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrPermanent = errors.New("retry: permanent error, do not retry")
ErrPermanent is a sentinel callers wrap into the error they return from op to short-circuit retries. The shared Do checks via errors.Is(err, ErrPermanent) after every attempt.
Go 1.20+ supports multiple %w verbs in fmt.Errorf so the underlying SDK error stays unwrappable via errors.As:
if !shouldRetry(awsErr) {
return fmt.Errorf("%w: %w", retry.ErrPermanent, awsErr)
}
Callers can then errors.Is(err, retry.ErrPermanent) AND errors.As(err, &awsSpecificErr) on the same returned value.
Functions ¶
func Do ¶
Do runs op until it returns nil or the attempt budget is exhausted. The per-attempt context (if PerAttemptTimeout > 0) is bounded separately from the outer deadline so a slow attempt fails fast and the outer retry budget continues, but it is still canceled when ctx is. That timeout only cancels the context passed to op, so op must honor ctx for a slow attempt to fail fast. Errors marked with errors.Is(err, ErrPermanent) short-circuit retries.
Returned error: nil on success, ctx.Err() if the outer context is canceled mid-backoff, the unwrapped op error if it short-circuits via ErrPermanent, or a wrapped "after N attempts: …" of the last op error if the budget exhausts.
Types ¶
type Config ¶
type Config struct {
// OnAttempt, if non-nil, is invoked once per attempt — see OnAttemptFn.
OnAttempt OnAttemptFn
// MaxAttempts is the total number of attempts (NOT retries). 1 = no
// retry, 5 = first attempt + 4 retries.
MaxAttempts int
// BaseDelay is the delay before the SECOND attempt; subsequent
// delays double until MaxDelay caps them.
BaseDelay time.Duration
// MaxDelay caps the per-iteration backoff, including any jitter.
MaxDelay time.Duration
// PerAttemptTimeout, if > 0, wraps each op invocation in a
// context.WithTimeout derived from the outer ctx, so a single hung
// attempt fails fast and the retry budget continues, while outer
// cancellation or deadline still cancels it. It only cancels the
// context passed to op; op must honor that ctx for an attempt to fail
// fast. Zero disables.
PerAttemptTimeout time.Duration
// Jitter, if true, adds ±25% noise to each backoff (the result is still
// capped at MaxDelay) via math/rand/v2
// (lock-free per-goroutine source, no global RNG mutex contention
// under concurrent retries). math/rand/v2 is sufficient for backoff
// jitter; cryptographic randomness is not needed.
Jitter bool
}
Config bundles the exponential-backoff knobs.
type OnAttemptFn ¶
OnAttemptFn is called BEFORE each attempt (after any backoff delay has elapsed, just before op runs). It receives the attempt number (1-indexed) and the error from the previous attempt (nil on the first call). Used for per-call-site logging that the shared helper can't know about.
Why "before" not "after": call-sites today log either while about-to-wait ("retrying after Nms") or right after a failure. The shared callback fires before each attempt and gets prevErr, so call-sites guard on `prevErr != nil` to log only on retries (matching today's behavior: no log on the first/only attempt). The first-attempt invocation with prevErr=nil is a no-op for most call-sites.