// Package errors provides utilities for error handling, including a flexible retry mechanism. package errors import ( "context" "math/rand" "time" ) // BackoffStrategy defines the interface for calculating retry delays. type BackoffStrategy interface { // Backoff returns the delay for a given attempt based on the base delay. Backoff(attempt int, baseDelay time.Duration) time.Duration } // ConstantBackoff provides a fixed delay for each retry attempt. type ConstantBackoff struct{} // Backoff returns the base delay regardless of the attempt number. // Implements BackoffStrategy with a constant delay. func (c ConstantBackoff) Backoff(_ int, baseDelay time.Duration) time.Duration { return baseDelay } // ExponentialBackoff provides an exponentially increasing delay for retry attempts. type ExponentialBackoff struct{} // Backoff returns a delay that doubles with each attempt, starting from the base delay. // Uses bit shifting for efficient exponential growth (e.g., baseDelay * 2^(attempt-1)). func (e ExponentialBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration { if attempt <= 1 { return baseDelay } return baseDelay * time.Duration(1< r.maxDelay { currentDelay = r.maxDelay } if r.jitter { currentDelay = addJitter(currentDelay) } // Wait with respect to context cancellation or timeout select { case <-r.ctx.Done(): return r.ctx.Err() case <-time.After(currentDelay): } } return lastErr } // Transform creates a new Retry instance with modified configuration. // Copies all settings from the original Retry and applies the given options. func (r *Retry) Transform(opts ...RetryOption) *Retry { newRetry := &Retry{ maxAttempts: r.maxAttempts, delay: r.delay, maxDelay: r.maxDelay, retryIf: r.retryIf, onRetry: r.onRetry, backoff: r.backoff, jitter: r.jitter, ctx: r.ctx, } for _, opt := range opts { opt(newRetry) } return newRetry } // WithBackoff sets the backoff strategy using the BackoffStrategy interface. // Returns a RetryOption; no-op if strategy is nil, retaining the existing strategy. func WithBackoff(strategy BackoffStrategy) RetryOption { return func(r *Retry) { if strategy != nil { r.backoff = strategy } } } // WithContext sets the context for cancellation and deadlines. // Returns a RetryOption; retains context.Background if ctx is nil. func WithContext(ctx context.Context) RetryOption { return func(r *Retry) { if ctx != nil { r.ctx = ctx } } } // WithDelay sets the initial delay between retries. // Returns a RetryOption; ensures non-negative delay by setting negatives to 0. func WithDelay(delay time.Duration) RetryOption { return func(r *Retry) { if delay < 0 { delay = 0 } r.delay = delay } } // WithJitter enables or disables jitter in the backoff delay. // Returns a RetryOption; toggles random delay variation. func WithJitter(jitter bool) RetryOption { return func(r *Retry) { r.jitter = jitter } } // WithMaxAttempts sets the maximum number of retry attempts. // Returns a RetryOption; ensures at least 1 attempt by adjusting lower values. func WithMaxAttempts(maxAttempts int) RetryOption { return func(r *Retry) { if maxAttempts < 1 { maxAttempts = 1 } r.maxAttempts = maxAttempts } } // WithMaxDelay sets the maximum delay between retries. // Returns a RetryOption; ensures non-negative delay by setting negatives to 0. func WithMaxDelay(maxDelay time.Duration) RetryOption { return func(r *Retry) { if maxDelay < 0 { maxDelay = 0 } r.maxDelay = maxDelay } } // WithOnRetry sets a callback to execute after each failed attempt. // Returns a RetryOption; callback receives attempt number and error. func WithOnRetry(onRetry func(attempt int, err error)) RetryOption { return func(r *Retry) { r.onRetry = onRetry } } // WithRetryIf sets the condition under which to retry. // Returns a RetryOption; retains IsRetryable default if retryIf is nil. func WithRetryIf(retryIf func(error) bool) RetryOption { return func(r *Retry) { if retryIf != nil { r.retryIf = retryIf } } } // ExecuteReply runs the provided function with retry logic and returns its result. // Returns the result and nil on success, or zero value and last error on failure; generic type T. func ExecuteReply[T any](r *Retry, fn func() (T, error)) (T, error) { var lastErr error var zero T for attempt := 1; attempt <= r.maxAttempts; attempt++ { result, err := fn() if err == nil { return result, nil } // Check if retry is applicable; return immediately if not retryable if r.retryIf != nil && !r.retryIf(err) { return zero, err } lastErr = err if r.onRetry != nil { r.onRetry(attempt, err) } if attempt == r.maxAttempts { break } // Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled currentDelay := r.backoff.Backoff(attempt, r.delay) if currentDelay > r.maxDelay { currentDelay = r.maxDelay } if r.jitter { currentDelay = addJitter(currentDelay) } // Wait with respect to context cancellation or timeout select { case <-r.ctx.Done(): return zero, r.ctx.Err() case <-time.After(currentDelay): } } return zero, lastErr }