-
Notifications
You must be signed in to change notification settings - Fork 5
/
retry.go
260 lines (229 loc) · 8.49 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
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
// Copyright 2015 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package retry
import (
"fmt"
"math"
"math/rand"
"time"
"github.com/juju/errors"
)
const (
// UnlimitedAttempts can be used as a value for Attempts to clearly
// show to the reader that there is no limit to the number of attempts.
UnlimitedAttempts = -1
)
// retryStopped is the error that is returned from the Call function
// when the stop channel has been closed.
type retryStopped struct {
lastError error
}
// Error provides the implementation for the error interface method.
func (e *retryStopped) Error() string {
return fmt.Sprintf("retry stopped")
}
// attemptsExceeded is the error that is returned when the retry count has
// been hit without the function returning a nil error result. The last error
// returned from the function being retried is available as the LastError
// attribute.
type attemptsExceeded struct {
lastError error
}
// Error provides the implementation for the error interface method.
func (e *attemptsExceeded) Error() string {
return fmt.Sprintf("attempt count exceeded: %s", e.lastError)
}
// durationExceeded is the error that is returned when the total time that the
// Call function would have executed exceeds the MaxDuration specified.
// The last error returned from the function being retried is available as the
// LastError attribute.
type durationExceeded struct {
lastError error
}
// Error provides the implementation for the error interface method.
func (e *durationExceeded) Error() string {
return fmt.Sprintf("max duration exceeded: %s", e.lastError)
}
// LastError retrieves the last error returned from Func before iteration
// was terminated due to the attempt count being exceeded, the maximum
// duration being exceeded, or the stop channel being closed.
func LastError(err error) error {
cause := errors.Cause(err)
switch err := cause.(type) {
case *attemptsExceeded:
return err.lastError
case *retryStopped:
return err.lastError
case *durationExceeded:
return err.lastError
}
return errors.Errorf("unexpected error type: %T, %s", cause, cause)
}
// IsAttemptsExceeded returns true if the error is the result of the Call
// function finishing due to hitting the requested number of Attempts.
func IsAttemptsExceeded(err error) bool {
cause := errors.Cause(err)
_, ok := cause.(*attemptsExceeded)
return ok
}
// IsDurationExceeded returns true if the error is the result of the Call
// function finishing due to the total duration exceeding the specified
// MaxDuration value.
func IsDurationExceeded(err error) bool {
cause := errors.Cause(err)
_, ok := cause.(*durationExceeded)
return ok
}
// IsRetryStopped returns true if the error is the result of the Call
// function finishing due to the stop channel being closed.
func IsRetryStopped(err error) bool {
cause := errors.Cause(err)
_, ok := cause.(*retryStopped)
return ok
}
// CallArgs is a simple structure used to define the behaviour of the Call
// function.
type CallArgs struct {
// Func is the function that will be retried if it returns an error result.
Func func() error
// IsFatalError is a function that, if set, will be called for every non-
// nil error result from Func. If IsFatalError returns true, the error
// is immediately returned breaking out from any further retries.
IsFatalError func(error) bool
// NotifyFunc is a function that is called if Func fails, and the attempt
// number. The first time this function is called attempt is 1, the second
// time, attempt is 2 and so on.
NotifyFunc func(lastError error, attempt int)
// Attempts specifies the number of times Func should be retried before
// giving up and returning the AttemptsExceeded error. If a negative
// value is specified, the Call will retry forever.
Attempts int
// Delay specifies how long to wait between retries.
Delay time.Duration
// MaxDelay specifies how longest time to wait between retries. If no
// value is specified there is no maximum delay.
MaxDelay time.Duration
// MaxDuration specifies the maximum time the Call function should spend
// iterating over Func. The duration is calculated from the start of the
// Call function. If the next delay time would take the total duration
// of the call over MaxDuration, then a DurationExceeded error is
// returned. If no value is specified, Call will continue until the number
// of attempts is complete.
MaxDuration time.Duration
// BackoffFunc allows the caller to provide a function that alters the
// delay each time through the loop. If this function is not provided the
// delay is the same each iteration. Alternatively a function such as
// retry.DoubleDelay can be used that will provide an exponential
// backoff. The first time this function is called attempt is 1, the
// second time, attempt is 2 and so on.
BackoffFunc func(delay time.Duration, attempt int) time.Duration
// Clock provides the mechanism for waiting. Normal program execution is
// expected to use something like clock.WallClock, and tests can override
// this to not actually sleep in tests.
Clock Clock
// Stop is a channel that can be used to indicate that the waiting should
// be interrupted. If Stop is nil, then the Call function cannot be interrupted.
// If the channel is closed prior to the Call function being executed, the
// Func is still attempted once.
Stop <-chan struct{}
}
// Validate the values are valid. The ensures that the Func, Delay, Attempts
// and Clock have been specified.
func (args *CallArgs) Validate() error {
if args.Func == nil {
return errors.NotValidf("missing Func")
}
if args.Delay == 0 {
return errors.NotValidf("missing Delay")
}
if args.Clock == nil {
return errors.NotValidf("missing Clock")
}
// One of Attempts or MaxDuration need to be specified
if args.Attempts == 0 && args.MaxDuration == 0 {
return errors.NotValidf("missing Attempts or MaxDuration")
}
return nil
}
// Call will repeatedly execute the Func until either the function returns no
// error, the retry count is exceeded or the stop channel is closed.
func Call(args CallArgs) error {
err := args.Validate()
if err != nil {
return errors.Trace(err)
}
start := args.Clock.Now()
for i := 1; args.Attempts <= 0 || i <= args.Attempts; i++ {
err = args.Func()
if err == nil {
return nil
}
if args.IsFatalError != nil && args.IsFatalError(err) {
return errors.Trace(err)
}
if args.NotifyFunc != nil {
args.NotifyFunc(err, i)
}
if i == args.Attempts && args.Attempts > 0 {
break // don't wait before returning the error
}
if args.BackoffFunc != nil {
delay := args.BackoffFunc(args.Delay, i)
if delay > args.MaxDelay && args.MaxDelay > 0 {
delay = args.MaxDelay
}
args.Delay = delay
}
elapsedTime := args.Clock.Now().Sub(start)
if args.MaxDuration > 0 && (elapsedTime+args.Delay) > args.MaxDuration {
return errors.Wrap(err, &durationExceeded{err})
}
// Wait for the delay, and retry
select {
case <-args.Clock.After(args.Delay):
case <-args.Stop:
return errors.Wrap(err, &retryStopped{err})
}
}
return errors.Wrap(err, &attemptsExceeded{err})
}
// DoubleDelay provides a simple function that doubles the duration passed in.
// This can then be easily used as the BackoffFunc in the CallArgs
// structure.
func DoubleDelay(delay time.Duration, attempt int) time.Duration {
if attempt == 1 {
return delay
}
return delay * 2
}
// ExpBackoff returns a function a which generates time.Duration values using
// an exponential back-off algorithm with the specified parameters. The
// returned value can then be easily used as the BackoffFunc in the CallArgs
// structure.
//
// The next delay value is calculated using the following formula:
//
// newDelay = min(minDelay * exp^attempt, maxDelay)
//
// If applyJitter is set to true, the function will randomly select and return
// back a value in the [minDelay, newDelay] range.
func ExpBackoff(minDelay, maxDelay time.Duration, exp float64, applyJitter bool) func(time.Duration, int) time.Duration {
minDelayF := float64(minDelay)
maxDelayF := float64(maxDelay)
return func(_ time.Duration, attempt int) time.Duration {
newDelay := minDelayF * math.Pow(exp, float64(attempt))
// Return a random value in the [minDelay, newDelay) range.
if applyJitter {
// We want to go +/- 20%, which is a 40% swing, and
// Float64 returns in the range 0-1
newDelay = (1 + rand.Float64()*0.4 - 0.2) * newDelay
}
if newDelay < minDelayF {
newDelay = minDelayF
}
if newDelay > maxDelayF {
newDelay = maxDelayF
}
return time.Duration(newDelay).Round(time.Millisecond)
}
}