Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Retry

Go Report Card Go Reference License: MIT

Retry is a context-aware retry loop driven by a composable Strategy. A Strategy is a plain function that controls the wait before the next attempt. Returning a negative duration stops retrying. Custom strategies are straightforward to write inline, and built-in strategies compose cleanly.

  • Composable strategiesExponential, Constant, Linear, wrapped by MaxAttempts
  • Sentinel errors — signal hard stops without consulting the strategy
  • Context-aware — cancellation is checked before every attempt and during sleep
  • Zero dependencies

Install

go get lowbit.dev/retry

A Strategy

A Strategy is a plain function:

type Strategy func(attempt int) time.Duration

attempt is 1-indexed: 1 after the first failure, 2 after the second. Return a positive duration to sleep before the next attempt, zero to retry immediately, or a negative value to stop. That's the whole contract.

Usage

err := retry.Do(ctx,
    retry.MaxAttempts(5, retry.Exponential(100*time.Millisecond, 30*time.Second)),
    func() error {
        return db.Ping(ctx)
    },
)

Or store the strategy and call it as a method:

strategy := retry.MaxAttempts(5, retry.Exponential(100*time.Millisecond, 30*time.Second))
err := strategy.Do(ctx, func() error {
    return db.Ping(ctx)
})

Custom strategies are just functions — no interface to implement:

err := retry.Do(ctx, func(attempt int) time.Duration {
    if attempt >= 3 {
        return -1 // stop
    }
    return time.Duration(attempt) * 200 * time.Millisecond
}, fn)

Strategies

retry ships with 5 basic strategies which should cover most regular usecases.

Strategy Behaviour
Exponential(base, cap) Full-jitter exponential backoff, uniform in [0, min(cap, base<<n)]
Constant(delay) Fixed wait before every retry
Linear(base) Wait grows as base × attempt (no jitter)
MaxAttempts(n, p) Wraps any strategy; stops after n total calls
Never Stops after the first failure; no retries
Always Retries immediately with no delay, indefinitely

All infinite strategies (Exponential, Constant, Linear, Always) are meant to be composed with MaxAttempts. The limit belongs there, not buried inside the strategy.

Sentinel Errors

Error Effect
ErrDoNotRetry Stops immediately; error returned as-is with sentinel in chain.
ErrRetryLimitExceeded Wrapped around the last error when the strategy returns negative.

Both are detectable with errors.Is:

err := retry.Do(ctx, strategy, fn)
switch {
case errors.Is(err, context.Canceled):
    // context was cancelled
case errors.Is(err, retry.ErrDoNotRetry):
    // fn signalled a hard stop
case errors.Is(err, retry.ErrRetryLimitExceeded):
    // strategy exhausted all attempts
}

To signal a hard stop from inside fn, wrap ErrDoNotRetry in the returned error:

func fetchUser(ctx context.Context, id int) error {
    resp, err := http.Get(...)
    if err != nil {
        return err // transient, will be retried
    }
    if resp.StatusCode == http.StatusNotFound {
        return fmt.Errorf("user %d not found: %w", id, retry.ErrDoNotRetry)
    }
    return nil
}

About

A context-aware retry loop driven by composable policies.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages