github.com/go-playground/pkg/v5@v5.29.1/errors/retrier.go (about) 1 //go:build go1.18 2 // +build go1.18 3 4 package errorsext 5 6 import ( 7 "context" 8 "time" 9 10 . "github.com/go-playground/pkg/v5/values/result" 11 ) 12 13 // MaxAttemptsMode is used to set the mode for the maximum number of attempts. 14 // 15 // eg. Should the max attempts apply to all errors, just ones not determined to be retryable, reset on retryable errors, etc. 16 type MaxAttemptsMode uint8 17 18 const ( 19 // MaxAttemptsNonRetryableReset will apply the max attempts to all errors not determined to be retryable, but will 20 // reset the attempts if a retryable error is encountered after a non-retryable error. 21 MaxAttemptsNonRetryableReset MaxAttemptsMode = iota 22 23 // MaxAttemptsNonRetryable will apply the max attempts to all errors not determined to be retryable. 24 MaxAttemptsNonRetryable 25 26 // MaxAttempts will apply the max attempts to all errors, even those determined to be retryable. 27 MaxAttempts 28 29 // MaxAttemptsUnlimited will not apply a maximum number of attempts. 30 MaxAttemptsUnlimited 31 ) 32 33 // BackoffFn is a function used to apply a backoff strategy to the retryable function. 34 // 35 // It accepts `E` in cases where the amount of time to backoff is dynamic, for example when and http request fails 36 // with a 429 status code, the `Retry-After` header can be used to determine how long to backoff. It is not required 37 // to use or handle `E` and can be ignored if desired. 38 type BackoffFn[E any] func(ctx context.Context, attempt int, e E) 39 40 // IsRetryableFn2 is called to determine if the type E is retryable. 41 type IsRetryableFn2[E any] func(ctx context.Context, e E) (isRetryable bool) 42 43 // EarlyReturnFn is the function that can be used to bypass all retry logic, no matter the MaxAttemptsMode, for when the 44 // type of `E` will never succeed and should not be retried. 45 // 46 // eg. If retrying an HTTP request and getting 400 Bad Request, it's unlikely to ever succeed and should not be retried. 47 type EarlyReturnFn[E any] func(ctx context.Context, e E) (earlyReturn bool) 48 49 // Retryer is used to retry any fallible operation. 50 type Retryer[T, E any] struct { 51 isRetryableFn IsRetryableFn2[E] 52 isEarlyReturnFn EarlyReturnFn[E] 53 maxAttemptsMode MaxAttemptsMode 54 maxAttempts uint8 55 bo BackoffFn[E] 56 timeout time.Duration 57 } 58 59 // NewRetryer returns a new `Retryer` with sane default values. 60 // 61 // The default values are: 62 // - `MaxAttemptsMode` is `MaxAttemptsNonRetryableReset`. 63 // - `MaxAttempts` is 5. 64 // - `Timeout` is 0 no context timeout. 65 // - `IsRetryableFn` will always return false as `E` is unknown until defined. 66 // - `BackoffFn` will sleep for 200ms. It's recommended to use exponential backoff for production. 67 // - `EarlyReturnFn` will be None. 68 func NewRetryer[T, E any]() Retryer[T, E] { 69 return Retryer[T, E]{ 70 isRetryableFn: func(_ context.Context, _ E) bool { return false }, 71 maxAttemptsMode: MaxAttemptsNonRetryableReset, 72 maxAttempts: 5, 73 bo: func(ctx context.Context, attempt int, _ E) { 74 t := time.NewTimer(time.Millisecond * 200) 75 defer t.Stop() 76 select { 77 case <-ctx.Done(): 78 case <-t.C: 79 } 80 }, 81 } 82 } 83 84 // IsRetryableFn sets the `IsRetryableFn` for the `Retryer`. 85 func (r Retryer[T, E]) IsRetryableFn(fn IsRetryableFn2[E]) Retryer[T, E] { 86 if fn == nil { 87 fn = func(_ context.Context, _ E) bool { return false } 88 } 89 r.isRetryableFn = fn 90 return r 91 } 92 93 // IsEarlyReturnFn sets the `EarlyReturnFn` for the `Retryer`. 94 // 95 // NOTE: If the `EarlyReturnFn` and `IsRetryableFn` are both set and a conflicting `IsRetryableFn` will take precedence. 96 func (r Retryer[T, E]) IsEarlyReturnFn(fn EarlyReturnFn[E]) Retryer[T, E] { 97 r.isEarlyReturnFn = fn 98 return r 99 } 100 101 // MaxAttempts sets the maximum number of attempts for the `Retryer`. 102 // 103 // NOTE: Max attempts is optional and if not set will retry indefinitely on retryable errors. 104 func (r Retryer[T, E]) MaxAttempts(mode MaxAttemptsMode, maxAttempts uint8) Retryer[T, E] { 105 r.maxAttemptsMode, r.maxAttempts = mode, maxAttempts 106 return r 107 } 108 109 // Backoff sets the backoff function for the `Retryer`. 110 func (r Retryer[T, E]) Backoff(fn BackoffFn[E]) Retryer[T, E] { 111 if fn == nil { 112 fn = func(_ context.Context, _ int, _ E) {} 113 } 114 r.bo = fn 115 return r 116 } 117 118 // Timeout sets the timeout for the `Retryer`. This is the timeout per `RetyableFn` attempt and not the entirety 119 // of the `Retryer` execution. 120 // 121 // A timeout of 0 will disable the timeout and is the default. 122 func (r Retryer[T, E]) Timeout(timeout time.Duration) Retryer[T, E] { 123 r.timeout = timeout 124 return r 125 } 126 127 // Do will execute the provided functions code and automatically retry using the provided retry function. 128 func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E] { 129 var attempt int 130 remaining := r.maxAttempts 131 for { 132 var result Result[T, E] 133 if r.timeout == 0 { 134 result = fn(ctx) 135 } else { 136 ctx, cancel := context.WithTimeout(ctx, r.timeout) 137 result = fn(ctx) 138 cancel() 139 } 140 if result.IsErr() { 141 err := result.Err() 142 isRetryable := r.isRetryableFn(ctx, err) 143 if !isRetryable && r.isEarlyReturnFn != nil && r.isEarlyReturnFn(ctx, err) { 144 return result 145 } 146 147 switch r.maxAttemptsMode { 148 case MaxAttemptsUnlimited: 149 goto RETRY 150 case MaxAttemptsNonRetryableReset: 151 if isRetryable { 152 remaining = r.maxAttempts 153 goto RETRY 154 } else if remaining > 0 { 155 remaining-- 156 } 157 case MaxAttemptsNonRetryable: 158 if isRetryable { 159 goto RETRY 160 } else if remaining > 0 { 161 remaining-- 162 } 163 case MaxAttempts: 164 if remaining > 0 { 165 remaining-- 166 } 167 } 168 if remaining == 0 { 169 return result 170 } 171 172 RETRY: 173 r.bo(ctx, attempt, err) 174 attempt++ 175 continue 176 } 177 return result 178 } 179 }