main
1// Package retry provides exponential backoff with jitter.
2package retry
3
4import (
5 "context"
6 "errors"
7 "math/rand/v2"
8 "time"
9)
10
11// Config holds retry configuration.
12type Config struct {
13 MaxAttempts int
14 BaseDelay time.Duration
15 MaxDelay time.Duration
16 Multiplier float64
17}
18
19// DefaultConfig returns a Config with sensible defaults.
20func DefaultConfig() Config {
21 return Config{
22 MaxAttempts: 3,
23 BaseDelay: time.Second,
24 MaxDelay: 30 * time.Second,
25 Multiplier: 2.0,
26 }
27}
28
29// ErrMaxAttemptsExceeded indicates all retry attempts failed.
30var ErrMaxAttemptsExceeded = errors.New("max retry attempts exceeded")
31
32// Do executes fn with exponential backoff until it succeeds or max attempts reached.
33// The function should return nil on success, or an error to trigger a retry.
34// Context cancellation stops retries immediately.
35func Do(ctx context.Context, cfg Config, fn func() error) error {
36 var lastErr error
37 delay := cfg.BaseDelay
38
39 for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ {
40 lastErr = fn()
41 if lastErr == nil {
42 return nil
43 }
44
45 if ctx.Err() != nil {
46 return ctx.Err()
47 }
48
49 if attempt == cfg.MaxAttempts {
50 break
51 }
52
53 // Add jitter: 75% to 125% of delay
54 jitter := 0.75 + rand.Float64()*0.5
55 sleepDuration := time.Duration(float64(delay) * jitter)
56
57 select {
58 case <-ctx.Done():
59 return ctx.Err()
60 case <-time.After(sleepDuration):
61 }
62
63 // Increase delay for next attempt
64 delay = min(time.Duration(float64(delay)*cfg.Multiplier), cfg.MaxDelay)
65 }
66
67 return errors.Join(ErrMaxAttemptsExceeded, lastErr)
68}