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}