retry

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 7 Imported by: 0

README

retry

A flexible retry framework with configurable backoff strategies, max attempts, timeouts, and retryable-error filtering.

Features

  • Backoff strategies: exponential backoff (with jitter), fixed interval, no delay
  • Custom backoff: implement the Backoff interface for custom strategies
  • Max attempts: fixed count or unlimited (bounded by context/timeout)
  • Overall timeout: deadline across all attempts combined
  • Retryable errors: filter which errors should be retried via WithRetryIf
  • Callbacks: OnRetry, OnSuccess, OnError hooks for observability
  • Decorator pattern: wrap any function to produce a retried version
  • Context-aware: respects context cancellation and deadlines
  • Zero dependencies: only uses the standard library

Quick start

import "github.com/LingByte/ling-base/retry"

// Basic retry with exponential backoff
err := retry.Do(ctx, func(ctx context.Context) error {
    return callRemoteAPI(ctx)
},
    retry.WithMaxAttempts(5),
    retry.WithExponentialBackoff(100*time.Millisecond, 10*time.Second, 2.0, true),
)

// Only retry on transient errors
err := retry.Do(ctx, op,
    retry.WithMaxAttempts(3),
    retry.WithFixedInterval(500*time.Millisecond),
    retry.WithRetryIf(func(err error) bool {
        var netErr net.Error
        return errors.As(err, &netErr) && netErr.Timeout()
    }),
)

Backoff strategies

Exponential backoff
retry.WithExponentialBackoff(
    base:     100*time.Millisecond,  // initial delay
    maxDelay: 10*time.Second,        // upper bound (0 = no cap)
    factor:   2.0,                   // growth multiplier
    jitter:   true,                  // ±25% random jitter
)

Delay = base * factor^attempt, capped at maxDelay. With jitter enabled, a random ±25% offset is added to avoid thundering-herd.

Fixed interval
retry.WithFixedInterval(500 * time.Millisecond)

Constant delay between every attempt.

No backoff
retry.WithNoBackoff()

Retry immediately with zero delay.

Custom backoff
type LinearBackoff struct{ Step time.Duration }
func (l LinearBackoff) NextDelay(attempt int) time.Duration {
    return l.Step * time.Duration(attempt+1)
}

retry.WithBackoff(LinearBackoff{Step: 100 * time.Millisecond})

Decorator pattern

Wrap any function to produce a retried version:

retriedGet := retry.Decorate(
    func(ctx context.Context, url string) (*http.Response, error) {
        return http.Get(url)
    },
    retry.WithMaxAttempts(3),
    retry.WithFixedInterval(500*time.Millisecond),
)
resp, err := retriedGet(ctx, "https://example.com")

For functions with no arguments:

retriedFn := retry.Decorate0(
    func(ctx context.Context) error { return doWork(ctx) },
    retry.WithMaxAttempts(5),
)
err := retriedFn(ctx)

Options

Option Description
WithMaxAttempts(n) Total attempts including first (default: 3, <0 = unlimited)
WithTimeout(d) Overall deadline for all attempts (0 = no timeout)
WithBackoff(b) Custom backoff strategy
WithExponentialBackoff(...) Convenience: exponential backoff
WithFixedInterval(d) Convenience: fixed interval
WithNoBackoff() Convenience: retry immediately
WithRetryIf(fn) Predicate to filter retryable errors
WithOnRetry(fn) Callback before each retry
WithOnSuccess(fn) Callback after success
WithOnError(fn) Callback when all attempts exhausted

License

MIT

Documentation

Overview

Package retry provides a flexible retry framework with configurable backoff strategies, max attempts, timeouts, and retryable-error filtering.

Strategies

The package ships with two built-in backoff strategies:

  • ExponentialBackoff: delay = base * factor^attempt, capped at maxDelay, with optional jitter to avoid thundering-herd.
  • FixedInterval: a constant delay between every attempt.

Custom strategies can be created by implementing the Backoff interface.

Retryable errors

By default every non-nil error is retried. Use WithRetryIf to restrict retries to specific error types (e.g. transient network errors).

Basic usage

err := retry.Do(ctx, func(ctx context.Context) error {
    return callRemoteAPI(ctx)
},
    retry.WithMaxAttempts(5),
    retry.WithExponentialBackoff(100*time.Millisecond, 10*time.Second, 2.0, true),
    retry.WithRetryIf(func(err error) bool {
        var netErr net.Error
        return errors.As(err, &netErr) && netErr.Timeout()
    }),
)

Decorator pattern

Retry can wrap any func to produce a retried version:

retriedGet := retry.Decorate(
    func(ctx context.Context, url string) (*http.Response, error) {
        return http.Get(url)
    },
    retry.WithMaxAttempts(3),
    retry.WithFixedInterval(500*time.Millisecond),
)
resp, err := retriedGet(ctx, "https://example.com")

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMaxAttemptsExceeded is returned when all attempts are exhausted
	// and the last error is nil (e.g. OnRetry hook cancelled the loop).
	ErrMaxAttemptsExceeded = errors.New("retry: max attempts exceeded")

	// ErrNoOperation is returned when Do is called with a nil operation.
	ErrNoOperation = errors.New("retry: operation must not be nil")
)

Sentinel errors returned by the retry package.

Functions

func Decorate

func Decorate[T any, R any](
	fn func(ctx context.Context, args T) (R, error),
	opts ...Option,
) func(ctx context.Context, args T) (R, error)

Decorate wraps a function with retry logic, returning a new function with the same signature. This enables the decorator pattern:

retriedFn := retry.Decorate(
    func(ctx context.Context, x int) (int, error) { return risky(x) },
    retry.WithMaxAttempts(3),
    retry.WithFixedInterval(100*time.Millisecond),
)
result, err := retriedFn(ctx, 42)

func Decorate0

func Decorate0(
	fn func(ctx context.Context) error,
	opts ...Option,
) func(ctx context.Context) error

Decorate0 wraps a function with no arguments and no return value.

func Do

func Do(ctx context.Context, op Operation, opts ...Option) error

Do executes the given operation with retry logic according to opts.

The operation is called at least once. If it returns a non-nil error that satisfies RetryIf (or all errors if RetryIf is nil), the framework waits according to the Backoff strategy and tries again, up to MaxAttempts.

If the context is cancelled or the overall Timeout is reached, Do returns immediately with the context/timeout error (or the last operation error, whichever is more descriptive).

func IsMaxAttempts

func IsMaxAttempts(err error) bool

IsMaxAttempts reports whether err is (or wraps) ErrMaxAttemptsExceeded.

Types

type Backoff

type Backoff interface {
	NextDelay(attempt int) time.Duration
}

Backoff computes the delay before the next attempt. attempt is zero-based: 0 means the delay before the 1st retry (after the 1st attempt failed), 1 means the delay before the 2nd retry, etc.

type CircuitBreaker

type CircuitBreaker interface {
	Execute(ctx context.Context, op func(context.Context) error) error
}

CircuitBreaker is an optional interface that retry can integrate with. If set via WithCircuitBreaker, each attempt is wrapped in the breaker's Execute. When the breaker is open, the retry loop stops immediately with the breaker's error (e.g. ErrCircuitOpen).

The circuitbreaker package's *CircuitBreaker satisfies this interface.

type ExponentialBackoff

type ExponentialBackoff struct {
	Base     time.Duration // initial delay (must be > 0)
	MaxDelay time.Duration // upper bound (0 = no cap)
	Factor   float64       // multiplier per attempt (e.g. 2.0)
	Jitter   bool          // add ±25% random jitter
}

ExponentialBackoff delays grow exponentially: base * factor^attempt, capped at maxDelay. When jitter is enabled, a random fraction of the computed delay is added (±25%) to avoid synchronized retry storms.

func NewExponentialBackoff

func NewExponentialBackoff(base, maxDelay time.Duration, factor float64, jitter bool) *ExponentialBackoff

NewExponentialBackoff is a convenience constructor with sensible defaults.

  • base: initial delay
  • maxDelay: upper bound (use 0 for no cap)
  • factor: growth multiplier (typically 2.0)
  • jitter: enable random jitter

func (*ExponentialBackoff) NextDelay

func (e *ExponentialBackoff) NextDelay(attempt int) time.Duration

NextDelay returns the delay for the given attempt (0-based).

type FixedInterval

type FixedInterval struct {
	Interval time.Duration
}

FixedInterval is a backoff strategy that always returns the same delay.

func NewFixedInterval

func NewFixedInterval(interval time.Duration) *FixedInterval

NewFixedInterval creates a FixedInterval with the given delay. If interval <= 0 it defaults to 100ms.

func (*FixedInterval) NextDelay

func (f *FixedInterval) NextDelay(_ int) time.Duration

NextDelay returns the constant interval.

type NoBackoff

type NoBackoff struct{}

NoBackoff is a backoff strategy with zero delay — retry immediately.

func (NoBackoff) NextDelay

func (NoBackoff) NextDelay(_ int) time.Duration

NextDelay returns zero.

type Operation

type Operation func(ctx context.Context) error

Operation is a function that the retry framework will attempt to execute.

type Option

type Option func(*Options)

Option is a functional option for configuring Options.

func WithBackoff

func WithBackoff(b Backoff) Option

WithBackoff sets the backoff strategy.

func WithCircuitBreaker

func WithCircuitBreaker(cb CircuitBreaker) Option

WithCircuitBreaker wraps each attempt in the given circuit breaker. When the breaker is open, the retry loop stops immediately with the breaker's error. This enables the pattern: retry handles transient failures within a single call, while the circuit breaker prevents cascading failures across calls to the same dependency.

func WithExponentialBackoff

func WithExponentialBackoff(base, maxDelay time.Duration, factor float64, jitter bool) Option

WithExponentialBackoff is a convenience that sets an ExponentialBackoff.

func WithFixedInterval

func WithFixedInterval(interval time.Duration) Option

WithFixedInterval is a convenience that sets a FixedInterval backoff.

func WithMaxAttempts

func WithMaxAttempts(n int) Option

WithMaxAttempts sets the total number of attempts (including the first). Set to 0 or 1 for a single attempt. Set to a negative value for unlimited retries (until context cancellation or timeout).

func WithNoBackoff

func WithNoBackoff() Option

WithNoBackoff retries immediately with no delay.

func WithOnError

func WithOnError(fn func(attempts int, err error)) Option

WithOnError sets a callback invoked when all attempts are exhausted.

func WithOnRetry

func WithOnRetry(fn func(attempt int, err error)) Option

WithOnRetry sets a callback invoked before each retry.

func WithOnSuccess

func WithOnSuccess(fn func(attempts int)) Option

WithOnSuccess sets a callback invoked after a successful attempt.

func WithRetryIf

func WithRetryIf(fn func(error) bool) Option

WithRetryIf sets a predicate that filters which errors are retryable.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the overall deadline for all attempts combined.

type Options

type Options struct {
	// MaxAttempts is the total number of attempts (including the first).
	// 0 or 1 means a single attempt with no retries.
	// < 0 means retry indefinitely until ctx is cancelled or timeout.
	MaxAttempts int

	// Timeout is the overall deadline for all attempts combined.
	// 0 means no overall timeout (rely on the caller's context).
	Timeout time.Duration

	// Backoff strategy. If nil, NoBackoff is used.
	Backoff Backoff

	// RetryIf decides whether an error is retryable.
	// If nil, all non-nil errors are retried.
	RetryIf func(error) bool

	// CircuitBreaker wraps each attempt. If the breaker is open, the
	// retry loop stops immediately. Optional.
	CircuitBreaker CircuitBreaker

	// OnRetry is called before each retry attempt (not before the first).
	// attempt is the 1-based index of the upcoming attempt.
	OnRetry func(attempt int, err error)

	// OnSuccess is called after a successful attempt (if ever).
	// attempts is the total number of attempts made (>= 1).
	OnSuccess func(attempts int)

	// OnError is called after all attempts are exhausted with the last error.
	OnError func(attempts int, err error)
}

Options configures the retry behaviour.

Jump to

Keyboard shortcuts

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