tessl install tessl/golang-github-com-cenkalti--backoff-v4@4.3.1Exponential backoff algorithm implementation for retrying operations that may fail
Retry operations provide functions to automatically retry failed operations using backoff policies. All retry functions guarantee at least one execution attempt and handle permanent errors that should not be retried.
// Function type for operations executed by Retry() or RetryNotify()
// The operation will be retried using a backoff policy if it returns an error
type Operation func() error
// Generic function type for operations that return data along with error
// Used by RetryWithData() and RetryNotifyWithData()
type OperationWithData[T any] func() (T, error)Retries an operation until it succeeds or the backoff policy stops.
// Retry the operation until it does not return error or BackOff stops
// o is guaranteed to be run at least once
// If o returns a *PermanentError, the operation is not retried
// Sleeps the goroutine for the duration returned by BackOff after failure
func Retry(o Operation, b BackOff) errorimport "github.com/cenkalti/backoff/v4"
operation := func() error {
// Your operation that may fail
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return nil
}
err := backoff.Retry(operation, backoff.NewExponentialBackOff())
if err != nil {
log.Fatal(err)
}Like Retry but returns data from the operation along with any error.
// RetryWithData is like Retry but returns data in the response too
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error)import "github.com/cenkalti/backoff/v4"
type APIResponse struct {
Data string
Code int
}
operation := func() (APIResponse, error) {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return APIResponse{}, err
}
defer resp.Body.Close()
// Process response
var result APIResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return APIResponse{}, err
}
return result, nil
}
result, err := backoff.RetryWithData(operation, backoff.NewExponentialBackOff())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got data: %+v\n", result)// Notify is a callback function invoked on each retry attempt
// Receives the operation error and backoff delay
// NOTE: If backoff policy stops retrying, notify is not called
type Notify func(error, time.Duration)Retries with a callback invoked before each sleep.
// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep
func RetryNotify(operation Operation, b BackOff, notify Notify) errorimport (
"log"
"github.com/cenkalti/backoff/v4"
)
notifyFunc := func(err error, duration time.Duration) {
log.Printf("Error occurred: %v. Retrying in %v", err, duration)
}
operation := func() error {
// Your operation
return someAPICall()
}
err := backoff.RetryNotify(
operation,
backoff.NewExponentialBackOff(),
notifyFunc,
)Like RetryNotify but returns data from the operation.
// RetryNotifyWithData is like RetryNotify but returns data in the response too
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error)import (
"log"
"github.com/cenkalti/backoff/v4"
)
type Result struct {
Value string
}
notifyFunc := func(err error, duration time.Duration) {
log.Printf("Retry attempt failed: %v. Waiting %v", err, duration)
}
operation := func() (Result, error) {
// Your operation that returns data
return fetchData()
}
result, err := backoff.RetryNotifyWithData(
operation,
backoff.NewExponentialBackOff(),
notifyFunc,
)
if err != nil {
log.Fatal(err)
}For advanced use cases requiring custom timer implementations (e.g., testing).
// Timer interface for custom timer implementations
type Timer interface {
// Start the timer to fire after the given duration
// Can be called multiple times to reset the timer
Start(duration time.Duration)
// Stop the timer and free resources
Stop()
// C returns the channel that receives the time when timer fires
// Should return the same channel instance across calls
C() <-chan time.Time
}// RetryNotifyWithTimer calls notify function with the error and wait duration
// using the given Timer for each failed attempt before sleep
// A default timer using system timer is used when nil is passed
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) errorimport "github.com/cenkalti/backoff/v4"
// Use default timer (pass nil)
err := backoff.RetryNotifyWithTimer(
operation,
backoff.NewExponentialBackOff(),
notifyFunc,
nil, // Uses default timer
)
// Or provide custom timer implementation
customTimer := &MyCustomTimer{}
err := backoff.RetryNotifyWithTimer(
operation,
backoff.NewExponentialBackOff(),
notifyFunc,
customTimer,
)// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data
// in the response too
func RetryNotifyWithTimerAndData[T any](
operation OperationWithData[T],
b BackOff,
notify Notify,
t Timer,
) (T, error)Permanent errors signal that an operation should not be retried, regardless of the backoff policy.
// PermanentError signals that the operation should not be retried
type PermanentError struct {
Err error
}
// Returns the error string from wrapped error
func (e *PermanentError) Error() string
// Returns the wrapped error
func (e *PermanentError) Unwrap() error
// Checks if target is a PermanentError
func (e *PermanentError) Is(target error) bool// Permanent wraps the given err in a *PermanentError
// Returns nil if err is nil
func Permanent(err error) errorimport (
"errors"
"github.com/cenkalti/backoff/v4"
)
operation := func() error {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return err // Transient error, will retry
}
defer resp.Body.Close()
switch resp.StatusCode {
case 200:
return nil // Success
case 400, 401, 403, 404:
// Client errors - don't retry
return backoff.Permanent(
fmt.Errorf("client error: %d", resp.StatusCode),
)
case 429, 500, 502, 503:
// Server errors - retry
return fmt.Errorf("server error: %d", resp.StatusCode)
default:
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
}
err := backoff.Retry(operation, backoff.NewExponentialBackOff())
// Will immediately return on 400/401/403/404 without retrying
// Will retry on 429/500/502/503 and other errorsimport "github.com/cenkalti/backoff/v4"
operation := func() error {
result, err := performOperation()
if err != nil {
// Check for specific error conditions
if isClientError(err) {
return backoff.Permanent(err)
}
return err
}
// Success case - returning nil
return nil
}
// Permanent(nil) returns nil, so this is safe
err := backoff.Retry(operation, backoff.NewExponentialBackOff())import (
"errors"
"github.com/cenkalti/backoff/v4"
)
err := backoff.Retry(operation, b)
if err != nil {
var permanent *backoff.PermanentError
if errors.As(err, &permanent) {
log.Printf("Permanent failure: %v", permanent.Err)
} else {
log.Printf("Retry limit exceeded: %v", err)
}
}All retry functions automatically respect context cancellation when using WithContext wrapper.
import (
"context"
"time"
"github.com/cenkalti/backoff/v4"
)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
operation := func() error {
// Your operation
return performTask()
}
b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
err := backoff.Retry(operation, b)
if err != nil {
if ctx.Err() != nil {
log.Printf("Stopped due to context: %v", ctx.Err())
} else {
log.Printf("Operation failed: %v", err)
}
}Always use WithContext to prevent indefinite retries:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
err := backoff.Retry(operation, b)Combine backoff policies with WithMaxRetries for circuit breaker patterns:
b := backoff.WithMaxRetries(
backoff.NewExponentialBackOff(),
5, // Maximum 5 attempts
)Wrap non-retryable errors with Permanent():
if isClientError(err) {
return backoff.Permanent(err)
}
return err // Will retryUse RetryNotify to log or monitor retry attempts:
notifyFunc := func(err error, duration time.Duration) {
metrics.IncrementRetryCount()
log.Printf("Retry #%d after error: %v", attemptCount, err)
}
err := backoff.RetryNotify(operation, b, notifyFunc)Adjust backoff parameters based on your use case:
// For fast-failing APIs
b := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(5*time.Second),
backoff.WithMaxElapsedTime(30*time.Second),
)
// For slow external services
b := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(2*time.Second),
backoff.WithMaxInterval(2*time.Minute),
backoff.WithMaxElapsedTime(15*time.Minute),
)type APIClient struct {
baseURL string
backoff backoff.BackOff
}
func (c *APIClient) Get(ctx context.Context, path string) ([]byte, error) {
var result []byte
operation := func() error {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+path, nil)
if err != nil {
return backoff.Permanent(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return backoff.Permanent(fmt.Errorf("client error: %d", resp.StatusCode))
}
if resp.StatusCode != 200 {
return fmt.Errorf("server error: %d", resp.StatusCode)
}
result, err = io.ReadAll(resp.Body)
return err
}
b := backoff.WithContext(c.backoff, ctx)
err := backoff.Retry(operation, b)
return result, err
}func ConnectWithRetry(ctx context.Context, dsn string) (*sql.DB, error) {
var db *sql.DB
operation := func() error {
var err error
db, err = sql.Open("postgres", dsn)
if err != nil {
return err
}
// Verify connection
if err := db.PingContext(ctx); err != nil {
db.Close()
return err
}
return nil
}
b := backoff.WithContext(
backoff.NewExponentialBackOff(
backoff.WithMaxElapsedTime(1*time.Minute),
),
ctx,
)
err := backoff.Retry(operation, b)
return db, err
}