-
Notifications
You must be signed in to change notification settings - Fork 171
Expand file tree
/
Copy pathoptions.go
More file actions
389 lines (343 loc) · 10.5 KB
/
options.go
File metadata and controls
389 lines (343 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
package retry
import (
"context"
"math"
"math/rand"
"time"
)
// Function signature of retry if function
type RetryIfFunc func(error) bool
// Function signature of OnRetry function
type OnRetryFunc func(attempt uint, err error)
// DelayContext provides configuration values needed for delay calculation.
type DelayContext interface {
Delay() time.Duration
MaxJitter() time.Duration
MaxBackOffN() uint
MaxDelay() time.Duration
}
// DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts.
type DelayTypeFunc func(n uint, err error, config DelayContext) time.Duration
// Timer represents the timer used to track time for a retry.
type Timer interface {
After(time.Duration) <-chan time.Time
}
// retrierCore holds the core configuration and business logic for retry operations.
// this is then used by Retrier and RetrierWithData public APIs
type retrierCore struct {
attempts uint
attemptsForError map[error]uint
delay time.Duration
maxDelay time.Duration
maxJitter time.Duration
onRetry OnRetryFunc
retryIf RetryIfFunc
delayType DelayTypeFunc
lastErrorOnly bool
context context.Context
timer Timer
wrapContextErrorWithLastError bool
maxBackOffN uint // pre-computed for BackOffDelay, immutable after New()
}
// Delay implements DelayContext
func (r *retrierCore) Delay() time.Duration {
return r.delay
}
// MaxJitter implements DelayContext
func (r *retrierCore) MaxJitter() time.Duration {
return r.maxJitter
}
// MaxBackOffN implements DelayContext
func (r *retrierCore) MaxBackOffN() uint {
return r.maxBackOffN
}
// MaxDelay implements DelayContext
func (r *retrierCore) MaxDelay() time.Duration {
return r.maxDelay
}
// Retrier is for retry operations that return only an error.
type Retrier struct {
*retrierCore
}
// RetrierWithData is for retry operations that return data and an error.
type RetrierWithData[T any] struct {
*retrierCore
}
// Option represents an option for retry.
type Option func(*retrierCore)
func newRetrieerCore(opts ...Option) *retrierCore {
core := &retrierCore{
attempts: uint(10),
attemptsForError: make(map[error]uint),
delay: 100 * time.Millisecond,
maxJitter: 100 * time.Millisecond,
onRetry: func(n uint, err error) {},
retryIf: IsRecoverable,
delayType: CombineDelay(BackOffDelay, RandomDelay),
lastErrorOnly: false,
context: context.Background(),
timer: &timerImpl{},
}
for _, opt := range opts {
opt(core)
}
const maxBackOffN uint = 62
core.maxBackOffN = maxBackOffN
if core.delay < 0 {
core.delay = 0
} else if core.delay > 0 {
core.maxBackOffN = maxBackOffN - uint(math.Floor(math.Log2(float64(core.delay))))
}
return core
}
// New creates a new Retrier with the given options.
// The returned Retrier can be safely reused across multiple retry operations.
func New(opts ...Option) *Retrier {
return &Retrier{retrierCore: newRetrieerCore(opts...)}
}
// NewWithData creates a new RetrierWithData[T] with the given options.
// The returned retrier can be safely reused across multiple retry operations.
func NewWithData[T any](opts ...Option) *RetrierWithData[T] {
return &RetrierWithData[T]{retrierCore: newRetrieerCore(opts...)}
}
func emptyOption(r *retrierCore) {}
// return the direct last error that came from the retried function
// default is false (return wrapped errors with everything)
func LastErrorOnly(lastErrorOnly bool) Option {
return func(r *retrierCore) {
r.lastErrorOnly = lastErrorOnly
}
}
// Attempts set count of retry. Setting to 0 will retry until the retried function succeeds.
// default is 10
func Attempts(attempts uint) Option {
return func(r *retrierCore) {
r.attempts = attempts
}
}
// UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0).
func UntilSucceeded() Option {
return func(r *retrierCore) {
r.attempts = 0
}
}
// AttemptsForError sets count of retry in case execution results in given `err`
// Retries for the given `err` are also counted against total retries.
// The retry will stop if any of given retries is exhausted.
//
// added in 4.3.0
func AttemptsForError(attempts uint, err error) Option {
return func(r *retrierCore) {
r.attemptsForError[err] = attempts
}
}
// Delay set delay between retry
// default is 100ms
func Delay(delay time.Duration) Option {
return func(r *retrierCore) {
r.delay = delay
}
}
// MaxDelay set maximum delay between retry
// does not apply by default
func MaxDelay(maxDelay time.Duration) Option {
return func(r *retrierCore) {
r.maxDelay = maxDelay
}
}
// MaxJitter sets the maximum random Jitter between retries for RandomDelay
func MaxJitter(maxJitter time.Duration) Option {
return func(r *retrierCore) {
r.maxJitter = maxJitter
}
}
// DelayType set type of the delay between retries
// default is a combination of BackOffDelay and RandomDelay for exponential backoff with jitter
func DelayType(delayType DelayTypeFunc) Option {
if delayType == nil {
return emptyOption
}
return func(r *retrierCore) {
r.delayType = delayType
}
}
// BackOffDelay is a DelayType which increases delay between consecutive retries
func BackOffDelay(n uint, _ error, config DelayContext) time.Duration {
maxBackOffN := config.MaxBackOffN()
n--
if n > maxBackOffN {
n = maxBackOffN
}
return config.Delay() << n
}
// FixedDelay is a DelayType which keeps delay the same through all iterations
func FixedDelay(_ uint, _ error, config DelayContext) time.Duration {
return config.Delay()
}
// RandomDelay is a DelayType which picks a random delay up to maxJitter
func RandomDelay(_ uint, _ error, config DelayContext) time.Duration {
maxJitter := config.MaxJitter()
if maxJitter == 0 {
return 0
}
return time.Duration(rand.Int63n(int64(maxJitter)))
}
// CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc
func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc {
const maxInt64 = uint64(math.MaxInt64)
return func(n uint, err error, config DelayContext) time.Duration {
var total uint64
for _, delay := range delays {
total += uint64(delay(n, err, config))
if total > maxInt64 {
total = maxInt64
}
}
return time.Duration(total)
}
}
// FullJitterBackoffDelay is a DelayTypeFunc that calculates delay using exponential backoff
// with full jitter. The delay is a random value between 0 and the current backoff ceiling.
// Formula: sleep = random_between(0, min(cap, base * 2^attempt))
// It uses config.Delay as the base delay and config.MaxDelay as the cap.
func FullJitterBackoffDelay(n uint, err error, config DelayContext) time.Duration {
// Calculate the exponential backoff ceiling for the current attempt
backoffCeiling := float64(config.Delay()) * math.Pow(2, float64(n))
currentCap := float64(config.MaxDelay())
// If MaxDelay is set and backoffCeiling exceeds it, cap at MaxDelay
if currentCap > 0 && backoffCeiling > currentCap {
backoffCeiling = currentCap
}
// Ensure backoffCeiling is at least 0
if backoffCeiling < 0 {
backoffCeiling = 0
}
// Add jitter: random value between 0 and backoffCeiling
// rand.Int63n panics if argument is <= 0
if backoffCeiling <= 0 {
return 0 // No delay if ceiling is zero or negative
}
jitter := rand.Int63n(int64(backoffCeiling)) // #nosec G404 -- Using math/rand is acceptable for non-security critical jitter.
return time.Duration(jitter)
}
// OnRetry function callback are called each retry
//
// log each retry example:
//
// retry.New(
// retry.OnRetry(func(n uint, err error) {
// log.Printf("#%d: %s\n", n, err)
// }),
// ).Do(
// func() error {
// return errors.New("some error")
// },
// )
func OnRetry(onRetry OnRetryFunc) Option {
if onRetry == nil {
return emptyOption
}
return func(r *retrierCore) {
r.onRetry = onRetry
}
}
// RetryIf controls whether a retry should be attempted after an error
// (assuming there are any retry attempts remaining)
//
// skip retry if special error example:
//
// retry.New(
// retry.RetryIf(func(err error) bool {
// if err.Error() == "special error" {
// return false
// }
// return true
// }),
// ).Do(
// func() error {
// return errors.New("special error")
// },
// )
//
// By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`,
// so above example may also be shortened to:
//
// retry.New().Do(
// func() error {
// return retry.Unrecoverable(errors.New("special error"))
// },
// )
func RetryIf(retryIf RetryIfFunc) Option {
if retryIf == nil {
return emptyOption
}
return func(r *retrierCore) {
r.retryIf = retryIf
}
}
// Context allow to set context of retry
// default are Background context
//
// example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope)
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel()
//
// retry.New(
// retry.Context(ctx),
// ).Do(
// func() error {
// ...
// },
// )
func Context(ctx context.Context) Option {
return func(r *retrierCore) {
r.context = ctx
}
}
// WithTimer provides a way to swap out timer module implementations.
// This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration
// for retries.
//
// example of augmenting time.After with a print statement
//
// type struct MyTimer {}
//
// func (t *MyTimer) After(d time.Duration) <- chan time.Time {
// fmt.Print("Timer called!")
// return time.After(d)
// }
//
// retry.New(
// retry.WithTimer(&MyTimer{}),
// ).Do(
// func() error { ... },
// )
func WithTimer(t Timer) Option {
return func(r *retrierCore) {
r.timer = t
}
}
// WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the
// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when
// using a context to cancel / timeout
//
// default is false
//
// ctx, cancel := context.WithCancel(context.Background())
// defer cancel()
//
// retry.New(
// retry.Context(ctx),
// retry.Attempts(0),
// retry.WrapContextErrorWithLastError(true),
// ).Do(
// func() error {
// ...
// },
// )
func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option {
return func(r *retrierCore) {
r.wrapContextErrorWithLastError = wrapContextErrorWithLastError
}
}