-
Notifications
You must be signed in to change notification settings - Fork 0
/
backoff.go
254 lines (224 loc) · 6.1 KB
/
backoff.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
// Copyright (c) 2019 Hervé Gouchet. All rights reserved.
// Use of this source code is governed by the MIT License
// that can be found in the LICENSE file.
// Package backoff provides a Fibonacci backoff implementation.
package backoff
import (
"context"
"sync"
"time"
)
const (
// DefaultInterval is the default interval between 2 iterations.
DefaultInterval = 500 * time.Millisecond
)
type funcAlgorithm func() time.Duration
// fibonacci implements the Fibonacci suite.
func fibonacci() funcAlgorithm {
var (
a time.Duration
b time.Duration = 1
)
return func() time.Duration {
a, b = b, a+b
return a
}
}
// Func must be implemented by any function to be run by the Backoff.
type Func func(context.Context) error
// RetryerFunc is implemented by any backoff strategy.
type RetryerFunc func(ctx context.Context, f Func) (int, error)
// Do guarantees to execute at least once f if ctx is not already cancelled.
// As long as f return in success and the context not done, BackOff will continue to call it,
// with sleep duration based the Fibonacci suite and the BackOff's interval.
// It's implements the RetryerFunc interface.
func Do(ctx context.Context, f Func) (int, error) {
return New(ctx).Do(f)
}
// DoN does the same job as Do but limits the number of attempt to n.
// It's implements the RetryerFunc interface.
func DoN(ctx context.Context, attempt int, f Func) (int, error) {
return New(ctx).WithMaxAttempt(attempt).Do(f)
}
// DoUntil does the same job as Do but limits ctx by adjusting the deadline to be no later than d.
// It's implements the RetryerFunc interface.
func DoUntil(ctx context.Context, t time.Time, f Func) (int, error) {
return New(ctx).WithDeadline(t).Do(f)
}
// Retry retries the function f until it does not return error or BackOff stops.
// f is guaranteed to be run at least once, unless the context is already cancelled.
// It's implements the RetryerFunc interface.
func Retry(ctx context.Context, f Func) (int, error) {
return New(ctx).Retry(f)
}
// RetryN does the same as Retry but limits the number of attempt to n.
// It's implements the RetryerFunc interface.
func RetryN(ctx context.Context, attempt int, f Func) (int, error) {
return New(ctx).WithMaxAttempt(attempt).Retry(f)
}
// RetryUntil does the same job as Retry but limits ctx by adjusting the deadline to be no later than d.
// It's implements the RetryerFunc interface.
func RetryUntil(ctx context.Context, t time.Time, f Func) (int, error) {
return New(ctx).WithDeadline(t).Retry(f)
}
// New returns a new instance of Backoff.
func New(ctx context.Context) *Backoff {
if ctx == nil {
ctx = context.Background()
}
b := newBackoff()
b.ctx, b.cancel = context.WithCancel(ctx)
return b
}
func newBackoff() *Backoff {
return &Backoff{
interval: DefaultInterval,
err: make(chan error),
fib: fibonacci(),
}
}
// Backoff is a time.Duration and an attempt counter.
// It provides means to do and retry something based on the Fibonacci suite as trigger.
type Backoff struct {
ctx context.Context
cancel context.CancelFunc
err chan error
fib funcAlgorithm
attempt,
maxAttempt int
interval time.Duration
mu sync.RWMutex
}
// Attempt implements the Retryer interface.
func (b *Backoff) Attempt() int {
b.mu.RLock()
defer b.mu.RUnlock()
return b.attempt
}
// Do implements the Retryer interface.
func (b *Backoff) Do(f Func) (int, error) {
if b.fib == nil {
return b.Attempt(), context.Canceled
}
go b.run(f, false)
return b.done()
}
// Reset implements the Retryer interface.
func (b *Backoff) Reset() {
b.mu.Lock()
b.attempt = 0
b.fib = fibonacci()
b.interval = DefaultInterval
b.mu.Unlock()
}
// Retry implements the Retryer interface.
func (b *Backoff) Retry(f Func) (int, error) {
if b.fib == nil {
return b.Attempt(), context.Canceled
}
go b.run(f, true)
return b.done()
}
// WithDeadline implements the Retryer interface.
func (b *Backoff) WithDeadline(t time.Time) *Backoff {
b2 := b.copy()
b2.ctx, b2.cancel = context.WithDeadline(b.ctx, t)
return b2
}
// WithInterval implements the Retryer interface.
func (b *Backoff) WithInterval(d time.Duration) *Backoff {
if d > 0 {
b.mu.Lock()
b.interval = d
b.mu.Unlock()
}
return b
}
// WithMaxAttempt implements the Retryer interface.
func (b *Backoff) WithMaxAttempt(n int) *Backoff {
if n > -1 {
b.mu.Lock()
b.maxAttempt = n
b.mu.Unlock()
}
return b
}
// copy copies the Backoff to create a new one with the same behavior.
// It also takes care of the underlying mutex.
func (b *Backoff) copy() *Backoff {
b2 := newBackoff()
b.mu.Lock()
b2.interval = b.interval
b2.maxAttempt = b.maxAttempt
b.mu.Unlock()
return b2
}
// done waits the end of the job, done or cancelled.
func (b *Backoff) done() (int, error) {
defer b.cancel()
select {
case <-b.ctx.Done():
return b.Attempt(), ErrDeadlineExceeded
case err := <-b.err:
return b.Attempt(), err
}
}
// next increments the number of attempt, to validate or not the go to the next iteration.
func (b *Backoff) next() error {
b.mu.Lock()
defer b.mu.Unlock()
b.attempt++
if b.maxAttempt > 0 && b.attempt >= b.maxAttempt {
// Maximum number of attempt exceeded.
return ErrRetry
}
return nil
}
// run runs the Retryer strategy by using f as job to do and retry as mode.
func (b *Backoff) run(f Func, retry bool) {
var err, rrr error
for {
select {
case <-b.ctx.Done():
// Context cancelled.
return
default:
}
err = f(b.ctx)
switch {
case
// Do is finished when an error has occurred.
!retry && err != nil,
// Retry is finished when no error occurred.
retry && err == nil:
// Job done.
b.err <- err
return
}
// Tries to begin a new iteration.
rrr = b.next()
if rrr != nil {
b.err <- newErrRetry(err)
return
}
// Waiting before to run the next iteration.
rrr = b.sleep()
if rrr != nil {
b.err <- newErrRetry(err)
return
}
}
}
// sleep pauses the current goroutine for at least the duration of the interval
// multiplied by the current Fibonacci value.
func (b *Backoff) sleep() error {
b.mu.Lock()
d := b.fib() * b.interval
b.mu.Unlock()
if d < 0 {
// Bound exceeded.
return ErrRetry
}
time.Sleep(d)
return nil
}