resilience

package module
v0.3.8 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 10 Imported by: 2

README

jp-go-resilience

Generic retry and circuit breaker patterns for building resilient Go clients using generics.

Overview

jp-go-resilience provides a generic, reusable implementation of common resilience patterns for Go applications. Using Go 1.18+ generics, it works with any request/response types, making it suitable for HTTP clients, gRPC clients, database clients, or any other operation that needs fault tolerance.

Features

  • Generic interfaces: Works with any request/response types using Go generics
  • Retry patterns: Configurable retry with exponential, constant, or fibonacci backoff
  • Circuit breaker: Protects downstream services from cascading failures
  • Combined wrapper: Easy composition of retry and circuit breaker
  • Error classification: Flexible error classification for retry and circuit breaker decisions
  • HTTP support: Built-in HTTP status code classification
  • Integration: Works seamlessly with jp-go-errors package
  • Functional options: Clean, extensible configuration API
  • Context-aware: Full support for Go context cancellation and timeouts

Installation

go get github.com/JohnPlummer/jp-go-resilience

Quick Start

package main

import (
    "context"
    "log/slog"
    "net/http"
    "time"

    "github.com/JohnPlummer/jp-go-resilience"
)

// HTTPClient wraps http.Client to implement ResilientClient interface
type HTTPClient struct {
    client *http.Client
}

func (c *HTTPClient) Execute(ctx context.Context, req *http.Request) (*http.Response, error) {
    return c.client.Do(req.WithContext(ctx))
}

func main() {
    // Create base client
    baseClient := &HTTPClient{
        client: &http.Client{Timeout: 10 * time.Second},
    }

    // Wrap with both retry and circuit breaker
    resilientClient := resilience.CombineRetryAndCircuitBreaker(
        baseClient,
        resilience.DefaultRetryConfig(),
        resilience.DefaultCircuitBreakerConfig(),
        slog.Default(),
    )

    // Use the client
    req, _ := http.NewRequest("GET", "https://api.example.com/data", nil)
    resp, err := resilientClient.Execute(context.Background(), req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}

Usage Patterns

Retry Only

Use retry when you need to handle transient failures without circuit breaker protection.

resilientClient := resilience.NewRetryWrapper(
    baseClient,
    resilience.WithMaxAttempts(3),
    resilience.WithExponentialBackoff(time.Second, 30*time.Second),
)

When to use:

  • External API calls with occasional transient failures
  • Network requests that may timeout or fail intermittently
  • Operations where the service is generally stable

See: examples/retry_only.go

Circuit Breaker Only

Use circuit breaker when you want to fail fast and protect downstream services without retries.

resilientClient := resilience.NewCircuitBreakerWrapper(
    baseClient,
    resilience.WithMaxRequests(5),
    resilience.WithTimeout(60*time.Second),
    resilience.WithReadyToTrip(func(counts resilience.CircuitBreakerCounts) bool {
        failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
        return counts.Requests >= 5 && failureRatio >= 0.5
    }),
)

When to use:

  • Protecting a failing downstream service from overload
  • Preventing cascading failures in microservices
  • Operations where retrying would make things worse

See: examples/circuit_breaker_only.go

Use both retry and circuit breaker for comprehensive resilience. The circuit breaker protects the downstream service (inner layer), while retry handles transient failures (outer layer).

resilientClient := resilience.CombineRetryAndCircuitBreaker(
    baseClient,
    resilience.DefaultRetryConfig(),
    resilience.DefaultCircuitBreakerConfig(),
    slog.Default(),
)

When to use:

  • Production HTTP/gRPC clients
  • Database connection pools
  • Any critical external dependency

Layering: Circuit breaker wraps the base client (inner), retry wraps the circuit breaker (outer). This ensures:

  • Circuit breaker state is accurately maintained
  • Retries respect circuit breaker state (won't retry when circuit is open)
  • Failed retries contribute to circuit breaker trip logic

See: examples/combined.go

Custom Error Classification

Implement custom error classification to control which errors trigger retries or trip the circuit breaker.

type MyClassifier struct{}

func (c *MyClassifier) IsRetryable(err error) bool {
    // Custom retry logic
    return errors.Is(err, MyTransientError)
}

func (c *MyClassifier) ShouldTripCircuit(err error) bool {
    // Custom circuit breaker logic
    return errors.Is(err, MySevereError)
}

resilientClient := resilience.NewRetryWrapper(
    baseClient,
    resilience.WithErrorClassifier(&MyClassifier{}),
)

See: examples/custom_classifier.go

Migration Guide

From OpenAI Client Code

Before:

// Old OpenAI-specific retry code
client := &http.Client{Timeout: 30 * time.Second}

for attempt := 0; attempt < 3; attempt++ {
    resp, err := client.Do(req)
    if err != nil {
        if attempt < 2 {
            time.Sleep(time.Second * time.Duration(1<<attempt))
            continue
        }
        return nil, err
    }
    return resp, nil
}

After:

// Generic resilience wrapper
type HTTPClient struct {
    client *http.Client
}

func (c *HTTPClient) Execute(ctx context.Context, req *http.Request) (*http.Response, error) {
    return c.client.Do(req.WithContext(ctx))
}

baseClient := &HTTPClient{
    client: &http.Client{Timeout: 30 * time.Second},
}

resilientClient := resilience.CombineRetryAndCircuitBreaker(
    baseClient,
    resilience.DefaultRetryConfig(),
    resilience.DefaultCircuitBreakerConfig(),
    slog.Default(),
)

resp, err := resilientClient.Execute(ctx, req)

Benefits:

  • No manual retry loops
  • Automatic exponential backoff with jitter
  • Circuit breaker protection
  • Standardized error classification
  • Context support
  • Comprehensive logging

See: examples/openai_migration.go

Configuration

Retry Options
resilience.NewRetryWrapper(
    client,
    resilience.WithMaxAttempts(5),                                    // Total attempts (default: 3)
    resilience.WithExponentialBackoff(time.Second, 30*time.Second),   // Exponential with cap
    resilience.WithConstantBackoff(2*time.Second),                    // Constant delay
    resilience.WithFibonacciBackoff(time.Second, 30*time.Second),     // Fibonacci sequence
    resilience.WithErrorClassifier(customClassifier),                 // Custom error logic
    resilience.WithRetryLogger(logger),                               // Custom logger
)

Backoff Strategies:

  • Exponential (default): Delays double each retry (~1s, ~2s, ~4s, ~8s)
  • Constant: Same delay between retries (~2s, ~2s, ~2s)
  • Fibonacci: Delays follow fibonacci sequence (~1s, ~1s, ~2s, ~3s, ~5s)

All strategies include jitter to prevent thundering herd.

Circuit Breaker Options
resilience.NewCircuitBreakerWrapper(
    client,
    resilience.WithMaxRequests(5),                  // Requests in half-open state (default: 3)
    resilience.WithInterval(10*time.Second),        // Count reset interval (default: 10s)
    resilience.WithTimeout(60*time.Second),         // Open state timeout (default: 30s)
    resilience.WithReadyToTrip(tripFunc),           // Custom trip logic
    resilience.WithCircuitBreakerErrorClassifier(customClassifier),
    resilience.WithStateChangeHandler(stateFunc),   // State change callback
    resilience.WithCircuitBreakerLogger(logger),    // Custom logger
)

Default trip logic:

  • Requires at least 3 requests
  • Trips when failure rate >= 60%

Circuit States:

  • Closed: Normal operation, requests flow through
  • Open: Too many failures, requests fail immediately
  • Half-Open: Testing if service recovered, limited requests allowed
Error Classification
Built-in HTTP Classification

The HTTPStatusClassifier provides sensible defaults:

Retryable errors:

  • 429 (Rate Limited)
  • 500, 502, 503, 504 (Server Errors)
  • Network errors
  • Timeouts (via jp-go-errors)

Circuit breaker trip conditions:

  • 401, 403 (Authentication/Authorization)
  • 500, 502, 503, 504 (Server Errors)
  • Unknown errors

Non-retryable:

  • Context cancellation or deadline exceeded
  • 4xx errors (except 429)
Custom Classification
type MyClassifier struct{}

func (c *MyClassifier) IsRetryable(err error) bool {
    // Return true for errors that should trigger retry
    return true
}

func (c *MyClassifier) ShouldTripCircuit(err error) bool {
    // Return true for errors that should trip circuit breaker
    return false
}

wrapper := resilience.NewRetryWrapper(
    client,
    resilience.WithErrorClassifier(&MyClassifier{}),
)
Integration with jp-go-errors

Works seamlessly with jp-go-errors sentinel errors:

import pkgerrors "github.com/JohnPlummer/jp-go-errors"

// Automatically handled:
// - pkgerrors.ErrRateLimited -> retryable, doesn't trip circuit
// - pkgerrors.ErrTimeout -> retryable, doesn't trip circuit
// - pkgerrors.ErrUnauthorized -> not retryable, trips circuit

Testing

Unit Testing Your Client
type mockClient struct {
    executeFunc func(ctx context.Context, req Request) (Response, error)
}

func (m *mockClient) Execute(ctx context.Context, req Request) (Response, error) {
    return m.executeFunc(ctx, req)
}

func TestMyService(t *testing.T) {
    mock := &mockClient{
        executeFunc: func(ctx context.Context, req Request) (Response, error) {
            return Response{Data: "test"}, nil
        },
    }

    resilientClient := resilience.NewRetryWrapper(mock)
    // Test your service with the resilient client
}
Test Configuration

Use shorter timeouts and fewer attempts for tests:

testRetryConfig := &resilience.RetryConfig{
    MaxAttempts:  2,
    InitialDelay: 10 * time.Millisecond,
    MaxDelay:     50 * time.Millisecond,
}

testCBConfig := &resilience.CircuitBreakerConfig{
    MaxRequests: 2,
    Interval:    100 * time.Millisecond,
    Timeout:     200 * time.Millisecond,
}

testClient := resilience.CombineRetryAndCircuitBreaker(
    baseClient,
    testRetryConfig,
    testCBConfig,
    testLogger,
)
Simulating Failures
type failingClient struct {
    failCount int
    calls     int
}

func (c *failingClient) Execute(ctx context.Context, req Request) (Response, error) {
    c.calls++
    if c.calls <= c.failCount {
        return Response{}, errors.New("simulated failure")
    }
    return Response{Data: "success"}, nil
}

// Test retry behavior
client := &failingClient{failCount: 2}
wrapper := resilience.NewRetryWrapper(client, resilience.WithMaxAttempts(3))
resp, err := wrapper.Execute(ctx, req)
// Should succeed on third attempt

Examples

Complete working examples are available in the examples directory:

Run examples:

cd examples
go run retry_only.go
go run circuit_breaker_only.go
go run combined.go
go run custom_classifier.go
go run openai_migration.go

Architecture

The package uses the decorator pattern with generics:

  1. Base client: Implements ResilientClient[Req, Resp]
  2. Retry wrapper: Adds retry logic, wraps base client
  3. Circuit breaker wrapper: Adds circuit breaker, wraps base client
  4. Combined wrapper: Circuit breaker (inner) + Retry (outer)
  5. Error classifiers: Determine retry and circuit breaker behavior

This layered approach allows flexible composition of resilience patterns.

Request -> Retry -> Circuit Breaker -> Base Client -> External Service
           ^        ^
           |        |
           |        Protects service, fails fast when unhealthy
           |
           Handles transient failures, respects circuit state

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please ensure:

  1. Tests pass: ginkgo run ./...
  2. Linting passes: golangci-lint run
  3. Documentation is updated
  4. Examples demonstrate new features

Documentation

Overview

Package resilience provides generic retry and circuit breaker patterns for building resilient clients. It supports any request/response type using Go generics and integrates with jp-go-errors for standardized error handling.

Example (CombineRetryAndCircuitBreaker)

Example_combineRetryAndCircuitBreaker demonstrates using both retry and circuit breaker together.

// Create a mock client
client := &mockClient{
	executeFunc: func(ctx context.Context, req string) (string, error) {
		return "success", nil
	},
}

// Combine retry and circuit breaker with default configs
combined := resilience.CombineRetryAndCircuitBreaker(
	client,
	resilience.DefaultRetryConfig(),
	resilience.DefaultCircuitBreakerConfig(),
	slog.Default(),
)

// Execute request with both retry and circuit breaker protection
ctx := context.Background()
resp, err := combined.Execute(ctx, "test request")
if err != nil {
	fmt.Printf("Request failed: %v\n", err)
	return
}

fmt.Printf("Response: %s\n", resp)
Output:
Response: success
Example (CustomConfiguration)

Example_customConfiguration demonstrates custom retry and circuit breaker configuration.

client := &mockClient{
	executeFunc: func(ctx context.Context, req string) (string, error) {
		return "success", nil
	},
}

// Custom retry configuration
retryConfig := &resilience.RetryConfig{
	MaxAttempts:  5,
	Strategy:     resilience.RetryStrategyExponential,
	InitialDelay: 100 * time.Millisecond,
	MaxDelay:     5 * time.Second,
}

// Custom circuit breaker configuration
cbConfig := &resilience.CircuitBreakerConfig{
	MaxRequests: 5,
	Interval:    10 * time.Second,
	Timeout:     60 * time.Second,
	ReadyToTrip: func(counts resilience.CircuitBreakerCounts) bool {
		failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
		return counts.Requests >= 10 && failureRatio >= 0.5
	},
}

combined := resilience.CombineRetryAndCircuitBreaker(
	client,
	retryConfig,
	cbConfig,
	slog.Default(),
)

ctx := context.Background()
resp, err := combined.Execute(ctx, "test")
if err != nil {
	fmt.Printf("Failed: %v\n", err)
	return
}

fmt.Printf("Success: %s\n", resp)
Output:
Success: success

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewStatusCodeError

func NewStatusCodeError(statusCode int, err error) error

NewStatusCodeError creates a new StatusCodeError. This is useful when wrapping errors from systems that don't provide status codes.

Example:

err := doRequest()
if err != nil {
    return resilience.NewStatusCodeError(http.StatusServiceUnavailable, err)
}

Types

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	// ReadyToTrip is called with a copy of counts whenever a request fails in the closed state.
	// If ReadyToTrip returns true, the circuit breaker will be placed into the open state.
	// Default: trips after 3 requests with 60% failure rate
	ReadyToTrip func(counts CircuitBreakerCounts) bool

	// ErrorClassifier determines which errors should trip the circuit breaker.
	// Default: HTTPStatusClassifier with standard trip codes
	ErrorClassifier CircuitBreakerErrorClassifier

	// OnStateChange is called whenever the circuit breaker changes state.
	OnStateChange func(name string, from, to CircuitBreakerState)

	// Logger for circuit breaker operations.
	// Default: slog.Default()
	Logger *slog.Logger

	// Interval is the cyclic period of the closed state for the circuit breaker
	// to clear the internal counts. If 0, never clears.
	// Default: 10 seconds
	Interval time.Duration

	// Timeout is the period of the open state, after which the state becomes half-open.
	// Default: 30 seconds
	Timeout time.Duration

	// MaxRequests is the maximum number of requests allowed to pass through
	// when the circuit breaker is in the half-open state.
	// Default: 3
	MaxRequests uint32
}

CircuitBreakerConfig holds circuit breaker configuration options.

func DefaultCircuitBreakerConfig

func DefaultCircuitBreakerConfig() *CircuitBreakerConfig

DefaultCircuitBreakerConfig returns circuit breaker configuration with sensible defaults.

type CircuitBreakerCounts

type CircuitBreakerCounts struct {
	Requests             uint32
	TotalSuccesses       uint32
	TotalFailures        uint32
	ConsecutiveSuccesses uint32
	ConsecutiveFailures  uint32
}

CircuitBreakerCounts holds the internal counts of the circuit breaker.

type CircuitBreakerErrorClassifier

type CircuitBreakerErrorClassifier interface {
	// ShouldTripCircuit returns true if the error represents a failure serious enough
	// to open the circuit breaker and stop requests temporarily.
	ShouldTripCircuit(err error) bool
}

CircuitBreakerErrorClassifier determines whether an error should trip the circuit breaker. Implement this interface to customize circuit breaker behavior for your specific error types.

func DefaultCircuitBreakerErrorClassifier

func DefaultCircuitBreakerErrorClassifier() CircuitBreakerErrorClassifier

DefaultCircuitBreakerErrorClassifier provides reasonable defaults for circuit breaker tripping. It trips on authentication errors (401, 403) and server errors (5xx), but not on rate limits or timeouts which are transient.

type CircuitBreakerOption

type CircuitBreakerOption func(*CircuitBreakerConfig)

CircuitBreakerOption is a functional option for configuring circuit breaker behavior.

func WithCircuitBreakerErrorClassifier

func WithCircuitBreakerErrorClassifier(classifier CircuitBreakerErrorClassifier) CircuitBreakerOption

WithCircuitBreakerErrorClassifier sets a custom error classifier for circuit breaker decisions.

Example:

classifier := &MyCustomClassifier{}
resilience.WithCircuitBreakerErrorClassifier(classifier)

func WithCircuitBreakerLogger

func WithCircuitBreakerLogger(logger *slog.Logger) CircuitBreakerOption

WithCircuitBreakerLogger sets a custom logger for circuit breaker operations.

Example:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
resilience.WithCircuitBreakerLogger(logger)

func WithInterval

func WithInterval(interval time.Duration) CircuitBreakerOption

WithInterval sets the interval for clearing counts in closed state.

Example:

resilience.WithInterval(10 * time.Second)

func WithMaxRequests

func WithMaxRequests(maxRequests uint32) CircuitBreakerOption

WithMaxRequests sets the maximum number of requests in half-open state.

Example:

resilience.WithMaxRequests(5)

func WithReadyToTrip

func WithReadyToTrip(fn func(counts CircuitBreakerCounts) bool) CircuitBreakerOption

WithReadyToTrip sets a custom function to determine when to trip the circuit.

Example:

resilience.WithReadyToTrip(func(counts resilience.CircuitBreakerCounts) bool {
    failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
    return counts.Requests >= 5 && failureRatio >= 0.5
})

func WithStateChangeHandler

func WithStateChangeHandler(fn func(name string, from, to CircuitBreakerState)) CircuitBreakerOption

WithStateChangeHandler sets a callback for circuit breaker state changes.

Example:

resilience.WithStateChangeHandler(func(name string, from, to resilience.CircuitBreakerState) {
    log.Printf("Circuit %s changed from %s to %s", name, from, to)
})

func WithTimeout

func WithTimeout(timeout time.Duration) CircuitBreakerOption

WithTimeout sets the timeout for staying in open state.

Example:

resilience.WithTimeout(60 * time.Second)

type CircuitBreakerState

type CircuitBreakerState int

CircuitBreakerState represents the state of the circuit breaker.

const (
	// StateClosed means the circuit is closed and requests flow normally.
	StateClosed CircuitBreakerState = iota

	// StateHalfOpen means the circuit is testing if the service has recovered.
	StateHalfOpen

	// StateOpen means the circuit is open and requests are rejected immediately.
	StateOpen
)

func (CircuitBreakerState) String

func (s CircuitBreakerState) String() string

String returns the string representation of the circuit breaker state.

type CircuitBreakerWrapper

type CircuitBreakerWrapper[Req, Resp any] struct {
	// contains filtered or unexported fields
}

CircuitBreakerWrapper wraps a ResilientClient with circuit breaker functionality. It tracks failures and opens the circuit when too many failures occur, preventing requests from reaching a failing downstream service.

func NewCircuitBreakerWrapper

func NewCircuitBreakerWrapper[Req, Resp any](
	client ResilientClient[Req, Resp],
	opts ...CircuitBreakerOption,
) *CircuitBreakerWrapper[Req, Resp]

NewCircuitBreakerWrapper creates a new circuit breaker wrapper around a ResilientClient. It applies the provided options to configure circuit breaker behavior.

Example:

wrapper := resilience.NewCircuitBreakerWrapper(
    client,
    resilience.WithMaxRequests(5),
    resilience.WithTimeout(60*time.Second),
)

func (*CircuitBreakerWrapper[Req, Resp]) Counts

func (w *CircuitBreakerWrapper[Req, Resp]) Counts() CircuitBreakerCounts

Counts returns the current counts of the circuit breaker.

func (*CircuitBreakerWrapper[Req, Resp]) Execute

func (w *CircuitBreakerWrapper[Req, Resp]) Execute(ctx context.Context, req Req) (Resp, error)

Execute executes the request through the circuit breaker. If the circuit is open, requests are rejected immediately without calling the underlying client. Circuit breaker errors are wrapped with jperrors types for consistent error handling:

  • gobreaker.ErrOpenState becomes jperrors.ErrCircuitOpen
  • gobreaker.ErrTooManyRequests becomes jperrors.ErrCircuitTooManyRequests

func (*CircuitBreakerWrapper[Req, Resp]) GetHealth

func (w *CircuitBreakerWrapper[Req, Resp]) GetHealth() HealthStatus

GetHealth returns the health status of the circuit breaker.

func (*CircuitBreakerWrapper[Req, Resp]) State

func (w *CircuitBreakerWrapper[Req, Resp]) State() CircuitBreakerState

State returns the current state of the circuit breaker.

type ErrorClassifier

type ErrorClassifier interface {
	// IsRetryable returns true if the error represents a transient failure
	// that should be retried.
	IsRetryable(err error) bool
}

ErrorClassifier determines whether an error should trigger a retry. Implement this interface to customize retry behavior for your specific error types.

func DefaultErrorClassifier

func DefaultErrorClassifier() ErrorClassifier

DefaultErrorClassifier provides reasonable defaults for most use cases. It treats 5xx errors, 429 (rate limit), network errors, and timeouts as retryable. It trips the circuit on authentication errors and persistent server errors.

type HTTPError

type HTTPError interface {
	error
	StatusCode() int
}

HTTPError represents an error with an associated HTTP status code. Many HTTP client libraries provide errors that implement this interface.

type HTTPStatusClassifier

type HTTPStatusClassifier struct {
	// RetryableStatuses lists HTTP status codes that should trigger retries.
	// Defaults to 429, 500, 502, 503, 504 if nil.
	RetryableStatuses []int

	// CircuitTripStatuses lists HTTP status codes that should trip the circuit breaker.
	// Defaults to 401, 403, 500, 502, 503, 504 if nil.
	CircuitTripStatuses []int
}

HTTPStatusClassifier provides HTTP status code-based error classification. It classifies errors based on HTTP status codes, treating certain codes as retryable and others as circuit breaker trip conditions.

func NewHTTPStatusClassifier

func NewHTTPStatusClassifier() *HTTPStatusClassifier

NewHTTPStatusClassifier creates a new HTTPStatusClassifier with default status code mappings. Retryable: 429 (rate limit), 500, 502, 503, 504 (server errors) Circuit trip: 401, 403 (auth errors), 500, 502, 503, 504 (server errors)

func (*HTTPStatusClassifier) IsRetryable

func (c *HTTPStatusClassifier) IsRetryable(err error) bool

IsRetryable implements ErrorClassifier for HTTP status codes. It checks if the error has an HTTP status code that indicates a retryable condition.

func (*HTTPStatusClassifier) ShouldTripCircuit

func (c *HTTPStatusClassifier) ShouldTripCircuit(err error) bool

ShouldTripCircuit implements CircuitBreakerErrorClassifier for HTTP status codes. It checks if the error has an HTTP status code that indicates the circuit should trip.

type HealthStatus

type HealthStatus struct {
	// Healthy indicates whether the circuit breaker is in a healthy state.
	// True for closed and half-open states, false for open state.
	Healthy bool `json:"healthy"`

	// Status is a short string description of the state ("closed", "half-open", "open", "unknown").
	Status string `json:"status"`

	// State is the full string representation of the circuit breaker state.
	State string `json:"state"`

	// Requests is the total number of requests in the current interval.
	Requests uint32 `json:"requests"`

	// TotalSuccesses is the total number of successful requests.
	TotalSuccesses uint32 `json:"total_successes"`

	// TotalFailures is the total number of failed requests.
	TotalFailures uint32 `json:"total_failures"`

	// ConsecutiveFailures is the number of consecutive failures.
	ConsecutiveFailures uint32 `json:"consecutive_failures"`

	// ConsecutiveSuccesses is the number of consecutive successes.
	ConsecutiveSuccesses uint32 `json:"consecutive_successes"`
}

HealthStatus represents the health status of a circuit breaker. It provides a strongly-typed alternative to map[string]interface{} for health checks.

type ResilientClient

type ResilientClient[Req, Resp any] interface {
	// Execute performs a request and returns a response or error.
	// The context should be used to control timeouts and cancellation.
	Execute(ctx context.Context, req Req) (Resp, error)
}

ResilientClient defines a generic interface for executing requests with retry and circuit breaker support. Type parameters Req and Resp can be any types, making this suitable for HTTP clients, gRPC clients, database clients, or any other operation that needs resilience patterns.

Example:

type HTTPClient struct {
    client *http.Client
}

func (c *HTTPClient) Execute(ctx context.Context, req *http.Request) (*http.Response, error) {
    return c.client.Do(req.WithContext(ctx))
}

// Wrap with retry
resilientClient := resilience.NewRetryWrapper(
    httpClient,
    resilience.WithMaxAttempts(3),
    resilience.WithExponentialBackoff(time.Second, 30*time.Second),
)

func CombineRetryAndCircuitBreaker

func CombineRetryAndCircuitBreaker[Req, Resp any](
	client ResilientClient[Req, Resp],
	retryConfig *RetryConfig,
	cbConfig *CircuitBreakerConfig,
	logger *slog.Logger,
) ResilientClient[Req, Resp]

CombineRetryAndCircuitBreaker creates a wrapper with both retry and circuit breaker functionality. The circuit breaker is applied first (inner layer) to protect the underlying service, then retry logic is applied (outer layer) to handle transient failures. This layering ensures circuit breaker state is accurately maintained while providing resilience.

type RetryConfig

type RetryConfig struct {
	// ErrorClassifier determines which errors should trigger retries.
	// Default: HTTPStatusClassifier with standard retryable codes
	ErrorClassifier ErrorClassifier

	// Logger for retry operations.
	// Default: slog.Default()
	Logger *slog.Logger

	// Strategy defines the backoff strategy.
	// Default: RetryStrategyExponential
	Strategy RetryStrategy

	// InitialDelay is the delay before the first retry.
	// Default: 1 second
	InitialDelay time.Duration

	// MaxDelay is the maximum delay between retries (for exponential/fibonacci).
	// Default: 30 seconds
	MaxDelay time.Duration

	// Multiplier is the backoff multiplier for exponential strategy.
	// For exponential backoff, delay = initialDelay * (multiplier ^ attempt).
	// Default: 2.0 (doubling)
	// Common values: 1.5 (moderate growth), 2.0 (doubling), 3.0 (aggressive growth)
	Multiplier float64

	// MaxAttempts is the maximum number of attempts (including the initial request).
	// Default: 3
	MaxAttempts int
}

RetryConfig holds retry configuration options.

func DefaultRetryConfig

func DefaultRetryConfig() *RetryConfig

DefaultRetryConfig returns retry configuration with sensible defaults.

type RetryOption

type RetryOption func(*RetryConfig)

RetryOption is a functional option for configuring retry behavior.

func WithConstantBackoff

func WithConstantBackoff(delay time.Duration) RetryOption

WithConstantBackoff configures constant delay between retries with jitter. All retry delays will be approximately the same.

Example:

resilience.WithConstantBackoff(2 * time.Second)
// Delays: ~2s, ~2s, ~2s, ~2s

func WithErrorClassifier

func WithErrorClassifier(classifier ErrorClassifier) RetryOption

WithErrorClassifier sets a custom error classifier for retry decisions.

Example:

classifier := &MyCustomClassifier{}
resilience.WithErrorClassifier(classifier)

func WithExponentialBackoff

func WithExponentialBackoff(initialDelay, maxDelay time.Duration) RetryOption

WithExponentialBackoff configures exponential backoff with jitter. Each retry delay is multiplied by the configured multiplier (default 2.0) up to maxDelay.

Example:

resilience.WithExponentialBackoff(time.Second, 30*time.Second)
// With default multiplier 2.0: ~1s, ~2s, ~4s, ~8s, ~16s, 30s (capped)

func WithFibonacciBackoff

func WithFibonacciBackoff(initialDelay, maxDelay time.Duration) RetryOption

WithFibonacciBackoff configures fibonacci backoff with jitter. Delays follow the fibonacci sequence up to maxDelay.

Example:

resilience.WithFibonacciBackoff(time.Second, 30*time.Second)
// Delays: ~1s, ~1s, ~2s, ~3s, ~5s, ~8s, ~13s, ~21s, 30s (capped)

func WithMaxAttempts

func WithMaxAttempts(attempts int) RetryOption

WithMaxAttempts sets the maximum number of retry attempts. The total number of calls will be MaxAttempts (including the initial attempt).

Example:

resilience.WithMaxAttempts(5) // Try up to 5 times total

func WithMultiplier

func WithMultiplier(multiplier float64) RetryOption

WithMultiplier sets the backoff multiplier for exponential strategy. Only applies when using RetryStrategyExponential.

Example:

resilience.WithMultiplier(1.5) // 50% growth per retry
// With InitialDelay=1s: ~1s, ~1.5s, ~2.25s, ~3.375s, ...

func WithRetryLogger

func WithRetryLogger(logger *slog.Logger) RetryOption

WithRetryLogger sets a custom logger for retry operations.

Example:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
resilience.WithRetryLogger(logger)

type RetryStats

type RetryStats struct {
	// TotalAttempts is the total number of attempts made (including initial and retries)
	TotalAttempts int64

	// TotalRetries is the number of retry attempts (not including initial attempts)
	TotalRetries int64

	// TotalSuccesses is the number of successful operations
	TotalSuccesses int64

	// TotalFailures is the number of failed operations (after all retries exhausted)
	TotalFailures int64

	// LastAttemptTime is the time of the last attempt
	LastAttemptTime time.Time

	// LastError is the last error encountered (if any)
	LastError error
}

RetryStats holds statistics about retry operations.

type RetryStrategy

type RetryStrategy string

RetryStrategy defines the backoff strategy for retry operations.

const (
	// RetryStrategyExponential uses exponential backoff with jitter.
	RetryStrategyExponential RetryStrategy = "exponential"

	// RetryStrategyConstant uses a constant delay between retries with jitter.
	RetryStrategyConstant RetryStrategy = "constant"

	// RetryStrategyFibonacci uses fibonacci backoff with jitter.
	RetryStrategyFibonacci RetryStrategy = "fibonacci"
)

type RetryWrapper

type RetryWrapper[Req, Resp any] struct {
	// contains filtered or unexported fields
}

RetryWrapper wraps a ResilientClient with configurable retry logic. It uses exponential, constant, or fibonacci backoff strategies with jitter to prevent thundering herd problems.

func NewRetryWrapper

func NewRetryWrapper[Req, Resp any](
	client ResilientClient[Req, Resp],
	opts ...RetryOption,
) *RetryWrapper[Req, Resp]

NewRetryWrapper creates a new retry wrapper around a ResilientClient. It applies the provided options to configure retry behavior.

Example:

wrapper := resilience.NewRetryWrapper(
    client,
    resilience.WithMaxAttempts(5),
    resilience.WithExponentialBackoff(time.Second, 30*time.Second),
)

func (*RetryWrapper[Req, Resp]) Execute

func (w *RetryWrapper[Req, Resp]) Execute(ctx context.Context, req Req) (Resp, error)

Execute performs the request with retry logic. It will retry on retryable errors up to MaxAttempts times using the configured backoff strategy.

func (*RetryWrapper[Req, Resp]) GetRetryStats

func (w *RetryWrapper[Req, Resp]) GetRetryStats() RetryStats

GetRetryStats returns statistics about retry operations. This method is thread-safe and returns a snapshot of the current statistics.

type StatusCodeError

type StatusCodeError struct {
	Err  error
	Code int
}

StatusCodeError wraps an error with an HTTP status code. Use this when you need to add status code information to an existing error.

func (*StatusCodeError) Error

func (e *StatusCodeError) Error() string

Error implements the error interface.

func (*StatusCodeError) StatusCode

func (e *StatusCodeError) StatusCode() int

StatusCode returns the HTTP status code. This implements the HTTPError interface.

func (*StatusCodeError) Unwrap

func (e *StatusCodeError) Unwrap() error

Unwrap implements error unwrapping for errors.Is and errors.As.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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