retrier

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: MIT Imports: 6 Imported by: 3

README

retrier

codecov Go Reference

A simple, standalone Go package for retrying operations with exponential backoff and jitter support. Zero external dependencies.

Features

  • Generic API: Works with any return type using Go generics
  • Zero Friction: Accepts standard error - works seamlessly with stdlib and third-party packages
  • Functional Options: Clean, self-documenting configuration with sensible defaults
  • Exponential Backoff: Configurable backoff with multiplier and max duration
  • Jitter Support: Add randomness to backoff delays to avoid thundering herd
  • Flexible Retry Policies: Control which errors should be retried automatically
  • Default Retry Policy: Configure default behavior for standard errors
  • Context Cancellation: Support for graceful cancellation during backoff delays
  • Debug Logging: Optional logging interface for observability
  • Zero Dependencies: Uses only Go standard library

Installation

go get github.com/rohmanhakim/retrier

Quick Start

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
    
    "github.com/rohmanhakim/retrier"
)

func main() {
    // Define the operation to retry - just return standard errors!
    fn := func() (*http.Response, error) {
        return http.Get("https://api.example.com/data")
    }
    
    // Execute with retry using functional options
    ctx := context.Background()
    result := retrier.Retry(ctx, retrier.NewNoOpLogger(), fn,
        retrier.WithMaxAttempts(5),
        retrier.WithJitter(100*time.Millisecond),
        retrier.WithInitialDuration(1*time.Second),
        retrier.WithMultiplier(2.0),
        retrier.WithMaxDuration(30*time.Second),
    )
    
    // Style 1: Idiomatic Go (multiple return values)
    value, attempts, err := result.Decompose()
    if err != nil {
        fmt.Printf("Failed after %d attempts: %v\n", attempts, err)
        return
    }
    fmt.Printf("Success: %v (attempts: %d)\n", value, attempts)
}
Result Methods

The Result[T] type provides multiple ways to access the outcome:

Decompose() - Idiomatic Go

Returns a tuple for traditional error handling:

value, attempts, err := retrier.Retry(ctx, logger, fn).Decompose()
if err != nil {
    return fmt.Errorf("failed after %d attempts: %w", attempts, err)
}
UnwrapOr() - Fallback Pattern

Returns the value or a default if failed. Perfect for configuration loading:

// Beautiful, safe fallback logic in one line
cacheTTL := retrier.Retry(ctx, logger, fetchRemoteConfig).UnwrapOr(defaultTTL)
Unwrap() - Panic on Failure

Returns the value or panics. Use only when failure should crash:

value := retrier.Retry(ctx, logger, fn).Unwrap()
Classic Methods
result := retrier.Retry(ctx, logger, fn)
result.IsSuccess()   // bool
result.IsFailure()   // bool
result.Value()       // T (zero value if failed)
result.Err()         // error (nil if succeeded)
result.Attempts()    // int

Configuration Options

Functional Options

The Retry function accepts functional options for configuration. All options have sensible defaults:

Option Description Default
WithMaxAttempts(n int) Maximum number of retry attempts 3
WithJitter(d time.Duration) Random delay added to backoff 0 (no jitter)
WithInitialDuration(d time.Duration) Initial backoff duration 1 second
WithMultiplier(m float64) Backoff multiplier 2.0
WithMaxDuration(d time.Duration) Maximum backoff duration 1 minute
WithRetryPolicy(p RetryPolicy) Default retry policy for standard errors RetryPolicyAuto
Using Defaults
// Simple case - just want retries with defaults (3 attempts, 1s initial backoff, 2x multiplier)
result := retrier.Retry(ctx, logger, fn)
Custom Configuration
// All options customized
result := retrier.Retry(ctx, logger, fn,
    retrier.WithMaxAttempts(10),
    retrier.WithJitter(50*time.Millisecond),
    retrier.WithInitialDuration(100*time.Millisecond),
    retrier.WithMultiplier(1.5),
    retrier.WithMaxDuration(5*time.Minute),
    retrier.WithRetryPolicy(retrier.RetryPolicyNever),
)

Error Handling

Standard Errors (Default Behavior)

By default, standard errors are automatically retried with exponential backoff:

fn := func() (string, error) {
    // Standard error - will be auto-retried
    return "", errors.New("connection timeout")
}
Custom Retry Behavior

For fine-grained control, implement the RetryableError interface:

type NetworkError struct {
    msg       string
    transient bool
}

func (e *NetworkError) Error() string {
    return e.msg
}

func (e *NetworkError) RetryPolicy() retrier.RetryPolicy {
    if e.transient {
        return retrier.RetryPolicyAuto    // Auto retry with backoff
    }
    return retrier.RetryPolicyNever       // Don't retry
}

// Usage
fn := func() (string, error) {
    // This error will be auto-retried because it implements RetryPolicyAuto
    return "", &NetworkError{msg: "timeout", transient: true}
}
Default Retry Policy

Configure the default behavior for standard errors:

// Fail-fast mode: Only retry errors that explicitly implement RetryableError with RetryPolicyAuto
result := retrier.Retry(ctx, logger, fn,
    retrier.WithRetryPolicy(retrier.RetryPolicyNever),  // Standard errors won't be retried
)

Retry Policies

Policy Description
RetryPolicyAuto Error will be retried automatically with exponential backoff
RetryPolicyManual Error should not be auto-retried, but is eligible for manual retry
RetryPolicyNever Permanent failure, should not be retried at all
Behavior Matrix
Error Type DefaultRetryPolicy=Auto (default) DefaultRetryPolicy=Never
Standard error ✅ Auto-retry ❌ Stop immediately
RetryableError(Auto) ✅ Auto-retry ✅ Auto-retry
RetryableError(Manual) ❌ Stop immediately ❌ Stop immediately
RetryableError(Never) ❌ Stop immediately ❌ Stop immediately

Key insight: RetryableError always takes precedence over DefaultRetryPolicy.

Context Cancellation

The retry operation respects context cancellation. If the context is cancelled during a backoff delay, the operation stops immediately:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result := retrier.Retry(ctx, logger, fn,
    retrier.WithMaxAttempts(10),
    retrier.WithInitialDuration(1*time.Second),
)

if errors.Is(result.Err(), context.Canceled) {
    // Handle cancellation
}

Debug Logging

Implement the DebugLogger interface to add observability:

type MyLogger struct{}

func (l *MyLogger) Enabled() bool {
    return true
}

func (l *MyLogger) LogRetry(ctx context.Context, attempt, maxAttempts int, backoff time.Duration, err error) {
    if err != nil {
        log.Printf("Retry %d/%d: backoff=%v, error=%v", attempt, maxAttempts, backoff, err)
    } else {
        log.Printf("Success on attempt %d/%d", attempt, maxAttempts)
    }
}

Use retrier.NewNoOpLogger() for zero-overhead when logging is not needed.

API Reference

Types
// RetryPolicy defines automatic retry behavior
type RetryPolicy int

const (
    RetryPolicyAuto   RetryPolicy = iota // Auto retry with backoff
    RetryPolicyManual                    // Manual retry only
    RetryPolicyNever                     // No retry
)

// RetryableError interface for fine-grained retry control
// Optional: standard errors use DefaultRetryPolicy
type RetryableError interface {
    error
    RetryPolicy() RetryPolicy
}

// Result holds the outcome of a retry operation
type Result[T any] struct {
    // Contains value on success, zero value on failure
    // Contains error on failure, nil on success
    // Number of attempts made
}

// Result methods
func (r Result[T]) Decompose() (T, int, error)  // Returns tuple for idiomatic Go
func (r Result[T]) UnwrapOr(default T) T        // Returns value or default
func (r Result[T]) Unwrap() T                   // Returns value or panics
func (r Result[T]) IsSuccess() bool             // true if succeeded
func (r Result[T]) IsFailure() bool             // true if failed
func (r Result[T]) Value() T                    // value (zero if failed)
func (r Result[T]) Err() error                  // error (nil if succeeded)
func (r Result[T]) Attempts() int               // number of attempts

// DebugLogger interface for logging
type DebugLogger interface {
    Enabled() bool
    LogRetry(ctx context.Context, attempt, maxAttempts int, backoff time.Duration, err error)
}
Functions
// Retry executes fn with retry logic
// Accepts standard error - works with any function!
func Retry[T any](ctx context.Context, logger DebugLogger, fn func() (T, error), opts ...RetryOption) Result[T]

// Functional options
func WithMaxAttempts(n int) RetryOption
func WithJitter(d time.Duration) RetryOption
func WithInitialDuration(d time.Duration) RetryOption
func WithMultiplier(m float64) RetryOption
func WithMaxDuration(d time.Duration) RetryOption
func WithRetryPolicy(p RetryPolicy) RetryOption

// NewNoOpLogger creates a no-op logger (zero overhead)
func NewNoOpLogger() *NoOpLogger

// NewRetryError creates a retry error (use when you need explicit retry control)
func NewRetryError(cause RetryErrorCause, message string, policy RetryPolicy, wrapped error) *RetryError

License

MIT License - see LICENSE for details.

Documentation

Overview

Package retrier provides a generic retry mechanism with exponential backoff and jitter.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DebugLogger

type DebugLogger interface {
	// Enabled returns true if debug logging is enabled.
	// When false, the retry handler will skip logging entirely for efficiency.
	Enabled() bool

	// LogRetry logs retry attempts with backoff information.
	// Parameters:
	//   - ctx: context for the retry operation
	//   - attempt: current attempt number (1-based)
	//   - maxAttempts: maximum number of attempts allowed
	//   - backoff: the delay before the next retry (0 for success/exhausted)
	//   - err: the error that triggered the retry (nil on success)
	LogRetry(ctx context.Context, attempt int, maxAttempts int, backoff time.Duration, err error)
}

DebugLogger provides structured debug logging capabilities for the retry package. Users can implement this interface to provide custom logging behavior. When debug mode is disabled, use NoOpLogger for zero overhead.

type NoOpLogger

type NoOpLogger struct{}

NoOpLogger is a no-operation implementation of DebugLogger. It provides zero overhead when debug mode is disabled. All methods are empty and Enabled() always returns false.

func NewNoOpLogger

func NewNoOpLogger() *NoOpLogger

NewNoOpLogger creates a new NoOpLogger instance.

func (*NoOpLogger) Enabled

func (n *NoOpLogger) Enabled() bool

Enabled returns false - debug logging is disabled.

func (*NoOpLogger) LogRetry

func (n *NoOpLogger) LogRetry(_ context.Context, _ int, _ int, _ time.Duration, _ error)

LogRetry is a no-op.

type Result

type Result[T any] struct {
	// contains filtered or unexported fields
}

Result encapsulates the immutable outcome of a retry operation. It holds either a successful value or an error, along with metadata about the execution.

func NewFailureResult

func NewFailureResult[T any](err error, attempts int) Result[T]

NewFailureResult creates a Result representing a failed retry operation.

func NewSuccessResult

func NewSuccessResult[T any](value T, attempts int) Result[T]

NewSuccessResult creates a Result representing a successful retry operation.

func Retry

func Retry[T any](ctx context.Context, logger DebugLogger, fn func() (T, error), opts ...RetryOption) Result[T]

Retry executes the provided function with retry logic. It will retry the function up to MaxAttempts times, applying exponential backoff with jitter between attempts.

Type parameter T represents the return type of the function being retried. Returns a Result containing the value (if successful), error (if failed), and the number of attempts made.

The ctx parameter allows cancellation of the retry operation. If the context is cancelled during a backoff delay, the function returns immediately with ErrContextCancelled.

The logger parameter provides debug logging capabilities. When debug mode is disabled (NoOpLogger), there is zero overhead from logging.

opts are functional options to configure retry behavior:

  • WithMaxAttempts(n int): Maximum retry attempts (default: 3)
  • WithJitter(d time.Duration): Random delay added to backoff (default: 0)
  • WithInitialDuration(d time.Duration): Initial backoff duration (default: 1s)
  • WithMultiplier(m float64): Backoff multiplier (default: 2.0)
  • WithMaxDuration(d time.Duration): Maximum backoff duration (default: 1m)
  • WithRetryPolicy(p RetryPolicy): Default retry policy for standard errors (default: RetryPolicyAuto)

Error handling:

  • If the error implements RetryableError, its RetryPolicy() is used
  • Standard errors use the configured DefaultRetryPolicy (defaults to RetryPolicyAuto)

Example:

result := retrier.Retry(ctx, logger, fn,
    retrier.WithMaxAttempts(5),
    retrier.WithJitter(100*time.Millisecond),
    retrier.WithInitialDuration(1*time.Second),
)

func (Result[T]) Attempts

func (r Result[T]) Attempts() int

Attempts returns the number of attempts made before success or failure.

func (Result[T]) Decompose added in v1.0.1

func (r Result[T]) Decompose() (T, int, error)

Decompose returns the result as a tuple (value, attempts, error). This provides idiomatic Go error handling for traditionalists:

value, attempts, err := retrier.Retry(ctx, logger, fn).Decompose()
if err != nil {
    // handle error
}

func (Result[T]) Err

func (r Result[T]) Err() error

Err returns the error if the operation failed, or nil if successful.

func (Result[T]) IsFailure

func (r Result[T]) IsFailure() bool

IsFailure returns true if the operation failed (has error).

func (Result[T]) IsSuccess

func (r Result[T]) IsSuccess() bool

IsSuccess returns true if the operation succeeded (no error).

func (Result[T]) Unwrap added in v1.0.1

func (r Result[T]) Unwrap() T

Unwrap returns the successful value or panics if failed. Use only when failure is impossible or should crash.

func (Result[T]) UnwrapOr added in v1.0.1

func (r Result[T]) UnwrapOr(defaultValue T) T

UnwrapOr returns the successful value, or the provided default if failed. Perfect for fallback configurations:

cacheTTL := retrier.Retry(ctx, logger, fetchRemoteConfig).UnwrapOr(defaultTTL)

func (Result[T]) Value

func (r Result[T]) Value() T

Value returns the successful result value. Returns zero value of T if the operation failed.

type RetryError

type RetryError struct {
	Message string
	Cause   RetryErrorCause
	// contains filtered or unexported fields
}

RetryError represents an error that occurred during retry attempts. It stores the original error for debugging and implements the RetryableError interface.

func NewRetryError

func NewRetryError(cause RetryErrorCause, message string, policy RetryPolicy, wrapped error) *RetryError

NewRetryError creates a new RetryError with explicit classification. Parameters:

  • cause: The error cause (ErrZeroAttempt or ErrExhaustedAttempts)
  • message: Human-readable error message
  • policy: RetryPolicyAuto, RetryPolicyManual, or RetryPolicyNever
  • wrapped: The original error that caused the retry failure (may be nil)

func (*RetryError) Error

func (e *RetryError) Error() string

Error returns the error message implementing the error interface.

func (*RetryError) Is

func (e *RetryError) Is(target error) bool

Is allows errors.Is to match RetryError types.

func (*RetryError) RetryPolicy

func (e *RetryError) RetryPolicy() RetryPolicy

RetryPolicy returns the automatic retry behavior for this error. When RetryError is returned (exhausted attempts), it returns the cached policy.

func (*RetryError) Unwrap

func (e *RetryError) Unwrap() error

Unwrap returns the wrapped error for error chain support.

type RetryErrorCause

type RetryErrorCause string

RetryErrorCause represents the cause of a retry error.

const (
	// ErrZeroAttempt indicates that MaxAttempts was set to 0.
	ErrZeroAttempt RetryErrorCause = "zero attempt"

	// ErrExhaustedAttempts indicates that all retry attempts were exhausted.
	ErrExhaustedAttempts RetryErrorCause = "exhausted attempt"

	// ErrContextCancelled indicates that the context was cancelled during retry.
	ErrContextCancelled RetryErrorCause = "context cancelled"
)

type RetryOption added in v1.0.1

type RetryOption func(*retryConfig)

RetryOption is a functional option for configuring retry behavior.

func WithInitialDuration added in v1.0.1

func WithInitialDuration(d time.Duration) RetryOption

WithInitialDuration sets the initial backoff duration. Default is 1 second.

func WithJitter added in v1.0.1

func WithJitter(d time.Duration) RetryOption

WithJitter sets the maximum random duration added to backoff delays. This helps avoid thundering herd problems. Default is 0 (no jitter).

func WithMaxAttempts added in v1.0.1

func WithMaxAttempts(n int) RetryOption

WithMaxAttempts sets the maximum number of retry attempts. Default is 3.

func WithMaxDuration added in v1.0.1

func WithMaxDuration(d time.Duration) RetryOption

WithMaxDuration sets the maximum backoff duration. Default is 1 minute.

func WithMultiplier added in v1.0.1

func WithMultiplier(m float64) RetryOption

WithMultiplier sets the backoff multiplier. Each subsequent delay is multiplied by this value. Default is 2.0.

func WithRetryPolicy added in v1.0.1

func WithRetryPolicy(p RetryPolicy) RetryOption

WithRetryPolicy sets the default retry policy for standard errors. Default is RetryPolicyAuto (standard errors are retried automatically).

type RetryPolicy

type RetryPolicy int

RetryPolicy defines automatic retry behavior. This controls whether Retry() will attempt exponential backoff.

const (
	// RetryPolicyAuto indicates the error should be retried automatically
	// with exponential backoff.
	RetryPolicyAuto RetryPolicy = iota

	// RetryPolicyManual indicates the error should not be auto-retried,
	// but is eligible for manual retry.
	RetryPolicyManual

	// RetryPolicyNever indicates a permanent failure that should not be retried.
	RetryPolicyNever
)

type RetryableError

type RetryableError interface {
	error

	// RetryPolicy controls automatic retry behavior.
	// Used by the retry handler to decide whether to retry.
	RetryPolicy() RetryPolicy
}

RetryableError is an interface that errors must implement to be handled by the retry mechanism. Users should implement this interface on their custom error types to control retry behavior.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL