retro

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 6 Imported by: 0

README

goioc/retro: Handy retry-library

goioc

Go go.dev reference codecov

ko-fi

Why another retry library?

There's a bunch of excellent retry-related Go libraries out there (and I took inspiration from some of them), like this one. Some of them are highly configurable (even more configurable than mine), but I was always lacking one feature: configurability by the error. I.e., most of the libraries allow you to configure the "retrier" in some or another way, making it behave the same way for all the "retriable" errors. What I needed for some of my usecases, is having different retry strategies for different errors. And that's why this library was made.

Basic usage

So, let's say we have a DB-related function that may return different types of errors: it returns sql.ErrNoRows if the result-set of the lookup is empty, and it returns driver.ErrBadConn if there's some transient connectivity error. In the first case, I want to retry with the constant rate (let's say, every second) and the maximum retry count of 3. In the second case, I want to have an exponential back-off starting with 10 milliseconds, but the retry delay should not exceed 1 second. I want to stop retrying after 5 seconds. Here's how one could implement it using the retro library:

	caller := NewCaller(). // instantiating the "retrier" aka "caller"
		WithRetriableError(sql.ErrNoRows,
			NewBackoffStrategy(NewConstant(1), time.Second).
				WithMaxRetries(3)). // constant back-off at the rate of 1 second and 3 max retries for sql.ErrNoRows
		WithRetriableError(driver.ErrBadConn,
			NewBackoffStrategy(NewExponential(2), 10*time.Millisecond).
				WithCappedDuration(time.Second)). // exponential back-off starting at 10 milliseconds with a 1-second cap
		WithMaxDuration(5 * time.Second) // maximum retry duration (across all retriable errors) - 5 seconds
	if err := caller.Call(context.TODO(), func() error {
		return queryDBFunction(...) // your function that runs a database query
	}); err != nil {
		panic(err)
	}

You can also specify a retry strategy for any error using WithRetryOnAnyError(...) on the caller. When configured, this takes precedence over error-specific rules.

Retry behavior

  • WithMaxRetries(N) allows the initial attempt plus N retries. This corrects the previous behavior, which counted the initial attempt toward the limit. Zero disables retries.
  • Error-specific rules use errors.Is(returnedError, registeredError), including wrapped and joined errors. The first matching rule in registration order wins.
  • Callers can be reused and called concurrently. Every Call starts fresh counters and generator sequences, with a separate budget for each registered rule. Configuration methods return independent callers.
  • Cancellation and WithMaxDuration interrupt backoff waits. Cancellation errors match both the context error and the last operation error, when one exists, using errors.Is. An already-canceled context prevents the initial attempt.
  • The callback runs synchronously. Neither cancellation nor WithMaxDuration can forcibly stop it. Pass an appropriate context to the work inside the callback; use a parent context with a timeout when the same deadline should apply to both the work and retries.
  • Growing sequences and duration arithmetic saturate at the int64 bounds instead of wrapping. Delays are clamped to zero and the configured cap after jitter.

Custom generators and validation

Built-in generators work with NewBackoffStrategy as before. Custom generators used with Caller must use NewBackoffStrategyWithFactory so every call can create independent state:

strategy := NewBackoffStrategyWithFactory(func() Generator {
	return &myGenerator{} // a fresh generator for every call
}, time.Millisecond).WithMaxRetries(3)

caller := NewCaller().WithRetryOnAnyError(strategy)

The factory must be safe to invoke concurrently and return a new, non-nil generator each time. A custom generator passed directly to NewBackoffStrategy still supports direct Delay() calls; using it with Caller returns a configuration error when a retry is needed. Direct Delay() calls on copies of the same strategy share one sequence and are serialized.

NewRandom(max) requires a positive maximum and panics at construction for invalid bounds, matching rand.Int63n's contract. Negative duration units, jitter, caps, and retry limits are reported as errors by Delay() (and propagated by Call when retrying). Jitter supports the full nonnegative time.Duration range. NewExponential starts at 1; NewLinear starts at 0.

More examples?

Please, take a look at the unit-tests for more examples.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrMaxRetries = errors.New("reached max retries")

ErrMaxRetries indicates that a strategy's retry budget has been exhausted.

Functions

This section is empty.

Types

type BackoffStrategy

type BackoffStrategy struct {
	// contains filtered or unexported fields
}

BackoffStrategy configures delays and retry limits. Caller starts a fresh sequence for every Call. Direct calls to Delay advance one sequence; copies of a strategy share that direct sequence and its retry counter.

func NewBackoffStrategy

func NewBackoffStrategy(generator Generator, durationUnit time.Duration) BackoffStrategy

NewBackoffStrategy multiplies generator's sequence by durationUnit. Built-in generators start from the beginning for each Call, regardless of prior use. Custom generators can be used directly with Delay; to use one with Caller, use NewBackoffStrategyWithFactory so each Call gets independent state. Negative duration units are reported as errors by Delay.

func NewBackoffStrategyWithFactory added in v1.1.1

func NewBackoffStrategyWithFactory(factory func() Generator, durationUnit time.Duration) BackoffStrategy

NewBackoffStrategyWithFactory constructs a strategy for a custom generator. factory must be safe to call concurrently and return a new, non-nil generator with independent state each time. It is called lazily on the first retry.

func (BackoffStrategy) Delay

func (b BackoffStrategy) Delay() (time.Duration, error)

Delay returns the next delay, or ErrMaxRetries once the configured number of delays has been returned. It reports invalid configuration as an error. Concurrent calls to Delay on the same strategy are serialized.

func (BackoffStrategy) WithCappedDuration

func (b BackoffStrategy) WithCappedDuration(cappedDuration time.Duration) BackoffStrategy

WithCappedDuration caps the final delay, including jitter. Negative caps are reported as errors by Delay.

func (BackoffStrategy) WithJitter

func (b BackoffStrategy) WithJitter(jitter time.Duration) BackoffStrategy

WithJitter adds uniform jitter in [-jitter, jitter) to each delay. Zero disables jitter. Negative values are reported as errors by Delay.

func (BackoffStrategy) WithMaxRetries

func (b BackoffStrategy) WithMaxRetries(maxRetries int64) BackoffStrategy

WithMaxRetries permits maxRetries retries after the initial attempt, per Call and per registered rule. Zero disables retries. Negative limits are reported as errors by Delay.

type Caller

type Caller struct {
	// contains filtered or unexported fields
}

Caller calls functions with the configured retry strategies. Configuration methods return independent callers. A configured Caller can be reused and its Call method can be used concurrently.

func NewCaller

func NewCaller() Caller

NewCaller constructs a Caller. The zero value is also ready to use.

func (Caller) Call

func (c Caller) Call(ctx context.Context, f func() error) error

Call invokes f until it succeeds, returns an unregistered error, exhausts its retry budget, or ctx is canceled. Retry state is independent for every Call. On cancellation, the returned error matches ctx.Err() and, if present, the last error from f through errors.Is. A successful f returns nil.

Call does not run f in a separate goroutine. To cancel work inside f, have f observe its own context (including any deadline needed for that work).

func (Caller) WithMaxDuration

func (c Caller) WithMaxDuration(maxDuration time.Duration) Caller

WithMaxDuration limits the time spent calling f and waiting between attempts. It interrupts backoff waits, but cannot interrupt f while it is executing. A nonpositive duration prevents f from being called.

func (Caller) WithRetriableError

func (c Caller) WithRetriableError(err error, strategy BackoffStrategy) Caller

WithRetriableError registers a strategy for errors matching err via errors.Is. Rules are matched in registration order; the first matching rule is used. Each rule has its own retry budget and generator for each Call.

func (Caller) WithRetryOnAnyError

func (c Caller) WithRetryOnAnyError(strategy BackoffStrategy) Caller

WithRetryOnAnyError retries all errors using strategy. When configured, it takes precedence over the rules registered with WithRetriableError.

type Generator

type Generator interface {
	// Next method returns a next number in the sequence.
	Next() int64
}

Generator produces a backoff sequence. Stateful generators should not be shared between goroutines. Caller creates its own generators for each Call.

func NewConstant

func NewConstant(c int64) Generator

NewConstant creates a Generator that always return the same number (specified as `c` parameter).

func NewExponential

func NewExponential(factor int64) Generator

NewExponential starts at 1 and multiplies by factor each time. Integer arithmetic preserves exact values and saturates at the int64 bounds.

func NewFibonacci

func NewFibonacci() Generator

NewFibonacci creates a Generator where every next number is a sum of two previous numbers (Fibonacci sequence). Starts with 1 and saturates at math.MaxInt64 on overflow.

func NewLinear

func NewLinear(delta int64) Generator

NewLinear starts at 0 and adds delta each time. Values saturate at the int64 bounds instead of wrapping on overflow.

func NewRandom

func NewRandom(max int64) Generator

NewRandom creates a Generator producing values in [0, max). It panics immediately if max is not positive, like rand.Int63n.

Jump to

Keyboard shortcuts

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