1 package backoff_test
2
3 import (
4 "context"
5 "fmt"
6 "io/ioutil"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/lestrrat-go/backoff/v2"
12 "github.com/stretchr/testify/assert"
13 )
14
15 func TestNull(t *testing.T) {
16 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
17 defer cancel()
18
19 p := backoff.Null()
20 c := p.Start(ctx)
21
22 var retries int
23 for backoff.Continue(c) {
24 t.Logf("%s backoff.Continue", time.Now())
25 retries++
26 }
27 if !assert.Equal(t, 1, retries, `should have done 1 retries`) {
28 return
29 }
30 }
31
32 func TestConstant(t *testing.T) {
33 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
34 defer cancel()
35
36 p := backoff.Constant(
37 backoff.WithInterval(300*time.Millisecond),
38 backoff.WithMaxRetries(4),
39 )
40 c := p.Start(ctx)
41
42 prev := time.Now()
43 var retries int
44 for backoff.Continue(c) {
45 t.Logf("%s backoff.Continue", time.Now())
46
47
48 retries++
49 if retries > 1 {
50 d := time.Since(prev)
51 if !assert.True(t, 350*time.Millisecond >= d && d >= 250*time.Millisecond, `timing is about 300ms (%s)`, d) {
52 return
53 }
54 }
55 prev = time.Now()
56 }
57
58
59 if !assert.Equal(t, 5, retries, `should have retried 5 times`) {
60 return
61 }
62 }
63
64 func isInErrorRange(expected, observed, margin time.Duration) bool {
65 return expected+margin > observed &&
66 observed > expected-margin
67 }
68
69 func TestExponential(t *testing.T) {
70 t.Run("Interval generator", func(t *testing.T) {
71 expected := []float64{
72 0.5, 0.75, 1.125, 1.6875, 2.53125, 3.796875,
73 }
74 ig := backoff.NewExponentialInterval()
75 for i := 0; i < len(expected); i++ {
76 if !assert.Equal(t, time.Duration(float64(time.Second)*expected[i]), ig.Next(), `interval for iteration %d`, i) {
77 return
78 }
79 }
80 })
81 t.Run("Jitter", func(t *testing.T) {
82 ig := backoff.NewExponentialInterval(
83 backoff.WithMaxInterval(time.Second),
84 backoff.WithJitterFactor(0.02),
85 )
86
87 testcases := []struct {
88 Base time.Duration
89 }{
90 {Base: 500 * time.Millisecond},
91 {Base: 750 * time.Millisecond},
92 {Base: time.Second},
93 }
94
95 for i := 0; i < 10; i++ {
96 dur := ig.Next()
97 var base time.Duration
98 if i > 2 {
99 base = testcases[2].Base
100 } else {
101 base = testcases[i].Base
102 }
103
104 min := int64(float64(base) * 0.98)
105 max := int64(float64(base) * 1.05)
106 t.Logf("max = %s, min = %s", time.Duration(max), time.Duration(min))
107 if !assert.GreaterOrEqual(t, int64(dur), min, "value should be greater than minimum") {
108 return
109 }
110 if !assert.GreaterOrEqual(t, max, int64(dur), "value should be less than maximum") {
111 return
112 }
113
114 }
115 })
116 t.Run("Back off, no jitter", func(t *testing.T) {
117 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
118 defer cancel()
119
120
121 expected := []float64{
122 0, 0.5, 0.7, 1.1, 1.6, 2.5, 3.7,
123 }
124 p := backoff.Exponential()
125 count := 0
126 prev := time.Now()
127 b := p.Start(ctx)
128 for backoff.Continue(b) {
129 now := time.Now()
130 d := now.Sub(prev)
131 d = d - d%(100*time.Millisecond)
132
133
134 expectedDuration := time.Duration(expected[count] * float64(time.Second))
135 if !assert.True(t, isInErrorRange(expectedDuration, d, 100*time.Millisecond), `observed duration (%s) should be whthin error range (expected = %s, range = %s)`, d, expectedDuration, 100*time.Millisecond) {
136 return
137 }
138 count++
139 if count == len(expected)-1 {
140 break
141 }
142 prev = now
143 }
144 })
145 }
146
147 func TestConcurrent(t *testing.T) {
148 if testing.Short() {
149 t.SkipNow()
150 }
151 t.Parallel()
152
153
154 testcases := []struct {
155 Policy backoff.Policy
156 Name string
157 }{
158 {Name: "Null", Policy: backoff.Null()},
159 {Name: "Exponential", Policy: backoff.Exponential(backoff.WithMultiplier(0.01), backoff.WithMinInterval(time.Millisecond))},
160 }
161
162 const max = 50
163 for _, tc := range testcases {
164 tc := tc
165
166 t.Run(tc.Name, func(t *testing.T) {
167 t.Parallel()
168 var wg sync.WaitGroup
169 wg.Add(max)
170 for i := 0; i < max; i++ {
171 go func(wg *sync.WaitGroup, b backoff.Policy) {
172 defer wg.Done()
173 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
174 defer cancel()
175 c := b.Start(ctx)
176 for backoff.Continue(c) {
177 fmt.Fprintf(ioutil.Discard, `Writing to the ether...`)
178 }
179 }(&wg, tc.Policy)
180 }
181 wg.Wait()
182 })
183 }
184 }
185
186 func TestConstantWithJitter(t *testing.T) {
187 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
188 defer cancel()
189
190 p := backoff.Constant(
191 backoff.WithInterval(300*time.Millisecond),
192 backoff.WithJitterFactor(0.50),
193 backoff.WithMaxRetries(999),
194 )
195 c := p.Start(ctx)
196
197 prev := time.Now()
198 var retries int
199 for backoff.Continue(c) {
200 t.Logf("%s backoff.Continue", time.Now())
201
202
203 retries++
204 if retries > 1 {
205 d := time.Since(prev)
206
207
208 if (150*time.Millisecond <= d && d < 250*time.Millisecond) ||
209 (350*time.Millisecond < d && d <= 450*time.Millisecond) {
210 break
211 }
212 }
213 prev = time.Now()
214 }
215
216
217 if !assert.NotEqual(t, 1000, retries, `should not have retried 1000 times; if the # of retries reaches 1000, probably jitter doesn't work'`) {
218 return
219 }
220 }
221
View as plain text