main
Raw Download raw file
 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}