1package retry
2
3import (
4 "sync"
5 "time"
6)
7
8// Backoff is an interface that backs off.
9type Backoff interface {
10 // Next returns the time duration to wait and whether to stop.
11 Next() (next time.Duration, stop bool)
12}
13
14var _ Backoff = (BackoffFunc)(nil)
15
16// BackoffFunc is a backoff expressed as a function.
17type BackoffFunc func() (time.Duration, bool)
18
19// Next implements Backoff.
20func (b BackoffFunc) Next() (time.Duration, bool) {
21 return b()
22}
23
24// WithJitter wraps a backoff function and adds the specified jitter. j can be
25// interpreted as "+/- j". For example, if j were 5 seconds and the backoff
26// returned 20s, the value could be between 15 and 25 seconds. The value can
27// never be less than 0.
28func WithJitter(j time.Duration, next Backoff) Backoff {
29 r := newLockedRandom(time.Now().UnixNano())
30
31 return BackoffFunc(func() (time.Duration, bool) {
32 val, stop := next.Next()
33 if stop {
34 return 0, true
35 }
36
37 diff := time.Duration(r.Int63n(int64(j)*2) - int64(j))
38 val = val + diff
39 if val < 0 {
40 val = 0
41 }
42 return val, false
43 })
44}
45
46// WithJitterPercent wraps a backoff function and adds the specified jitter
47// percentage. j can be interpreted as "+/- j%". For example, if j were 5 and
48// the backoff returned 20s, the value could be between 19 and 21 seconds. The
49// value can never be less than 0 or greater than 100.
50func WithJitterPercent(j uint64, next Backoff) Backoff {
51 r := newLockedRandom(time.Now().UnixNano())
52
53 return BackoffFunc(func() (time.Duration, bool) {
54 val, stop := next.Next()
55 if stop {
56 return 0, true
57 }
58
59 // Get a value between -j and j, the convert to a percentage
60 top := r.Int63n(int64(j)*2) - int64(j)
61 pct := 1 - float64(top)/100.0
62
63 val = time.Duration(float64(val) * pct)
64 if val < 0 {
65 val = 0
66 }
67 return val, false
68 })
69}
70
71// WithMaxRetries executes the backoff function up until the maximum attempts.
72func WithMaxRetries(max uint64, next Backoff) Backoff {
73 var l sync.Mutex
74 var attempt uint64
75
76 return BackoffFunc(func() (time.Duration, bool) {
77 l.Lock()
78 defer l.Unlock()
79
80 if attempt >= max {
81 return 0, true
82 }
83 attempt++
84
85 val, stop := next.Next()
86 if stop {
87 return 0, true
88 }
89
90 return val, false
91 })
92}
93
94// WithCappedDuration sets a maximum on the duration returned from the next
95// backoff. This is NOT a total backoff time, but rather a cap on the maximum
96// value a backoff can return. Without another middleware, the backoff will
97// continue infinitely.
98func WithCappedDuration(cap time.Duration, next Backoff) Backoff {
99 return BackoffFunc(func() (time.Duration, bool) {
100 val, stop := next.Next()
101 if stop {
102 return 0, true
103 }
104
105 if val <= 0 || val > cap {
106 val = cap
107 }
108 return val, false
109 })
110}
111
112// WithMaxDuration sets a maximum on the total amount of time a backoff should
113// execute. It's best-effort, and should not be used to guarantee an exact
114// amount of time.
115func WithMaxDuration(timeout time.Duration, next Backoff) Backoff {
116 start := time.Now()
117
118 return BackoffFunc(func() (time.Duration, bool) {
119 diff := timeout - time.Since(start)
120 if diff <= 0 {
121 return 0, true
122 }
123
124 val, stop := next.Next()
125 if stop {
126 return 0, true
127 }
128
129 if val <= 0 || val > diff {
130 val = diff
131 }
132 return val, false
133 })
134}