RetryFunc with capped exponential backoff and jitter

Go
package retry

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// RetryFunc calls fn until it succeeds, attempts run out, or ctx is done.
// Delays grow exponentially from base, capped at maxDelay, with full jitter.
func RetryFunc(ctx context.Context, attempts int, base, maxDelay time.Duration, fn func(context.Context) error) error {
    attempts = max(attempts, 1)
    var last error
    for i := 0; i < attempts; i++ {
        if err := ctx.Err(); err != nil {
            return err
        }
        if last = fn(ctx); last == nil {
            return nil
        }
        if i == attempts-1 {
            break
        }
        delay := min(base<<i, maxDelay)
        if delay > 0 {
            wait := time.Duration(rand.Int63n(int64(delay)))
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(wait):
            }
        }
    }
    return fmt.Errorf("retry: %d attempts failed: %w", attempts, last)
}