package retry import ( "context" "errors" "math" "time" ) // Config controls exponential backoff behavior. type Config struct { Attempts int Initial time.Duration MaxDelay time.Duration Factor float64 } // Retry calls fn until it succeeds, attempts are exhausted, or ctx is canceled. func Retry(ctx context.Context, cfg Config, fn func() error) error { if fn == nil { return errors.New("retry: nil function") } if cfg.Attempts <= 0 { return errors.New("retry: attempts must be positive") } if cfg.Initial < 0 || cfg.MaxDelay < 0 { return errors.New("retry: delays must not be negative") } if cfg.Factor < 1 { cfg.Factor = 2 } delay := cfg.Initial var lastErr error for attempt := 0; attempt < cfg.Attempts; attempt++ { if err := ctx.Err(); err != nil { return err } if lastErr = fn(); lastErr == nil { return nil } if attempt == cfg.Attempts-1 { break } if err := wait(ctx, delay); err != nil { return err } delay = nextDelay(delay, cfg.Factor, cfg.MaxDelay) } return lastErr } func wait(ctx context.Context, delay time.Duration) error { if delay <= 0 { return nil } timer := time.NewTimer(delay) defer timer.Stop() select { case <-timer.C: return nil case <-ctx.Done(): return ctx.Err() } } func nextDelay(current time.Duration, factor float64, maximum time.Duration) time.Duration { next := float64(current) * factor if next > float64(math.MaxInt64) { next = float64(math.MaxInt64) } delay := time.Duration(next) if maximum > 0 && delay > maximum { return maximum } return delay }