retry.go

  1// Package retry provides helpers for retrying.
  2//
  3// This package defines flexible interfaces for retrying Go functions that may
  4// be flakey or eventually consistent. It abstracts the "backoff" (how long to
  5// wait between tries) and "retry" (execute the function again) mechanisms for
  6// maximum flexibility. Furthermore, everything is an interface, so you can
  7// define your own implementations.
  8//
  9// The package is modeled after Go's built-in HTTP package, making it easy to
 10// customize the built-in backoff with your own custom logic. Additionally,
 11// callers specify which errors are retryable by wrapping them. This is helpful
 12// with complex operations where only certain results should retry.
 13package retry
 14
 15import (
 16	"context"
 17	"errors"
 18	"time"
 19)
 20
 21// RetryFunc is a function passed to [Do].
 22type RetryFunc func(ctx context.Context) error
 23
 24// RetryFuncValue is a function passed to [Do] which returns a value.
 25type RetryFuncValue[T any] func(ctx context.Context) (T, error)
 26
 27type retryableError struct {
 28	err error
 29}
 30
 31// RetryableError marks an error as retryable.
 32func RetryableError(err error) error {
 33	if err == nil {
 34		return nil
 35	}
 36	return &retryableError{err}
 37}
 38
 39// Unwrap implements error wrapping.
 40func (e *retryableError) Unwrap() error {
 41	return e.err
 42}
 43
 44// Error returns the error string.
 45func (e *retryableError) Error() string {
 46	if e.err == nil {
 47		return "retryable: <nil>"
 48	}
 49	return "retryable: " + e.err.Error()
 50}
 51
 52func DoValue[T any](ctx context.Context, b Backoff, f RetryFuncValue[T]) (T, error) {
 53	var nilT T
 54
 55	for {
 56		// Return immediately if ctx is canceled
 57		select {
 58		case <-ctx.Done():
 59			return nilT, ctx.Err()
 60		default:
 61		}
 62
 63		v, err := f(ctx)
 64		if err == nil {
 65			return v, nil
 66		}
 67
 68		// Not retryable
 69		var rerr *retryableError
 70		if !errors.As(err, &rerr) {
 71			return nilT, err
 72		}
 73
 74		next, stop := b.Next()
 75		if stop {
 76			return nilT, rerr.Unwrap()
 77		}
 78
 79		// ctx.Done() has priority, so we test it alone first
 80		select {
 81		case <-ctx.Done():
 82			return nilT, ctx.Err()
 83		default:
 84		}
 85
 86		t := time.NewTimer(next)
 87		select {
 88		case <-ctx.Done():
 89			t.Stop()
 90			return nilT, ctx.Err()
 91		case <-t.C:
 92			continue
 93		}
 94	}
 95}
 96
 97// Do wraps a function with a backoff to retry. The provided context is the same
 98// context passed to the [RetryFunc].
 99func Do(ctx context.Context, b Backoff, f RetryFunc) error {
100	_, err := DoValue(ctx, b, func(ctx context.Context) (*struct{}, error) {
101		return nil, f(ctx)
102	})
103	return err
104}