-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
retry.go
157 lines (124 loc) · 4.26 KB
/
retry.go
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
package goutil
import (
"context"
"errors"
"math"
"math/rand"
"time"
)
var ErrTimeout = errors.New("timed out waiting for the condition")
var ErrNotSetDeadline = errors.New("context doesn't set deadline")
var (
DefaultRetry = BackoffWait{TotalRuns: 2, BaseDuration: time.Second, Factor: 2.0, JitterFactor: 0.1}
FastRetry = BackoffWait{TotalRuns: 2, BaseDuration: 50 * time.Millisecond, Factor: 2.0, JitterFactor: 0.5}
UnlimitedRetry = BackoffWait{TotalRuns: math.MaxInt32, BaseDuration: time.Second, Factor: 2.0, JitterFactor: 0.5}
NoRetry = BackoffWait{TotalRuns: 1}
)
// BackoffWait encapsulates parameters that control the behavior of backoff mechanism.
// TotalRuns denotes the maximum number of times the function is executed,
// BaseDuration is the initial waiting time before function execution,
// Factor is the multiplier for exponential growth of waiting time,
// JitterFactor is the factor for random increase to the waiting time.
type BackoffWait struct {
TotalRuns int `json:"total_runs"`
BaseDuration time.Duration `json:"base_duration"`
Factor float64 `json:"factor"`
JitterFactor float64 `json:"jitter_factor"`
}
// wait returns a time duration to wait before next function execution.
// It updates the BackoffWait's BaseDuration and TotalRuns after each call.
func (b *BackoffWait) wait() time.Duration {
if b.TotalRuns < 1 {
if b.JitterFactor > 0 {
return addJitter(b.BaseDuration, b.JitterFactor)
}
return b.BaseDuration
}
b.TotalRuns--
duration := b.BaseDuration
if b.Factor != 0 {
b.BaseDuration = time.Duration(float64(b.BaseDuration) * b.Factor)
}
if b.JitterFactor > 0 {
duration = addJitter(duration, b.JitterFactor)
}
return duration
}
// addJitter adds random jitter to the base duration.
func addJitter(base time.Duration, jitterFactor float64) time.Duration {
if jitterFactor <= 0.0 {
jitterFactor = 1.0
}
return base + time.Duration(rand.Float64()*jitterFactor*float64(base))
}
// RetryableFunc is a function type that can be retried until it succeeds or meets a certain condition.
type RetryableFunc func() (done bool, err error)
// RetryableFuncWithContext is a RetryableFunc that includes context, which can be used for cancellation or passing request-scoped data.
type RetryableFuncWithContext func(context.Context) (done bool, err error)
func (cf RetryableFunc) WithContext() RetryableFuncWithContext {
return func(context.Context) (done bool, err error) {
return cf()
}
}
func exponentialBackoff(backoff BackoffWait, fn RetryableFunc) error {
for backoff.TotalRuns > 0 {
if done, err := fn(); err != nil || done {
return err
}
if backoff.TotalRuns == 1 {
break
}
time.Sleep(backoff.wait())
}
return ErrTimeout
}
func exponentialBackoffWithCtx(ctx context.Context, backoff BackoffWait, fnWithContext RetryableFuncWithContext) error {
for backoff.TotalRuns > 0 {
select {
case <-ctx.Done():
return ErrTimeout
default:
done, err := fnWithContext(ctx)
backoff.TotalRuns--
if err != nil || done {
return err
}
if backoff.TotalRuns <= 0 {
break
}
time.Sleep(backoff.wait())
}
if backoff.TotalRuns <= 0 {
break
}
}
return ErrTimeout
}
// RetryWithExponentialBackoff tries a function with exponential backoff, and return the error from the function or timeout.
func RetryWithExponentialBackoff(backoff BackoffWait, fn RetryableFunc) (err error) {
waitErr := exponentialBackoff(backoff, func() (bool, error) {
var done bool
done, err = fn()
return done, nil // swallow err in the process
})
if waitErr != nil && err == nil {
return waitErr
}
return err
}
// RetryWithExponentialBackoffUntilTimeout tries a function with exponential backoff until it succeeds or the context is cancelled (timeout).
func RetryWithExponentialBackoffUntilTimeout(ctx context.Context, f RetryableFuncWithContext) (err error) {
if _, ok := ctx.Deadline(); !ok {
return ErrNotSetDeadline
}
backOff := BackoffWait{TotalRuns: math.MaxInt32, BaseDuration: time.Second, Factor: 1.5, JitterFactor: 0.5}
waitErr := exponentialBackoffWithCtx(ctx, backOff, func(ctx context.Context) (bool, error) {
var done bool
done, err = f(ctx)
return done, nil // swallow err in the process
})
if waitErr != nil && err == nil {
return waitErr
}
return err
}