Skip to content

Commit 39b40b5

Browse files
committed
feat(delay): add IncrementalDelayMax method
This method will help in situations that the user want a more flexible delays between retries. For example, if the user wants to delay up to 10 seconds for some reason.
1 parent 3c92c52 commit 39b40b5

File tree

3 files changed

+139
-12
lines changed

3 files changed

+139
-12
lines changed

retry.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
// attempts. You can use the IncrementalDelay method to increment the delays
88
// between attempts. It gives a jitter to the delay to prevent Thundering herd
99
// problems. If the delay is 0 in either case, it does not sleep between tries.
10+
// The IncrementalDelay has a maximum delay of 1 second, but if you need a more
11+
// flexible delay, you can use the IncrementalDelayMax method and give it a max
12+
// delay.
1013
package retry
1114

1215
import (
@@ -38,6 +41,7 @@ func (s StopError) Unwrap() error { return s.Err }
3841
type Retry struct {
3942
Method DelayMethod
4043
Delay time.Duration
44+
MaxDelay time.Duration
4145
Attempts int
4246
}
4347

@@ -97,14 +101,23 @@ func StandardDelay(_ int, delay time.Duration) time.Duration { return delay }
97101
// adds a jitter to prevent Thundering herd. If the delay is 0, it always
98102
// returns 0.
99103
func IncrementalDelay(attempt int, delay time.Duration) time.Duration {
100-
if delay == 0 {
101-
return 0
102-
}
103-
if delay > time.Second {
104-
delay = time.Second
104+
return IncrementalDelayMax(time.Second)(attempt, delay)
105+
}
106+
107+
// IncrementalDelayMax returns a DelayMethod that increases the delay between
108+
// attempts up to the given max duration. It adds a jitter to prevent
109+
// Thundering herd. If the delay is 0, it always returns 0.
110+
func IncrementalDelayMax(max time.Duration) func(int, time.Duration) time.Duration {
111+
return func(attempt int, delay time.Duration) time.Duration {
112+
if delay == 0 {
113+
return 0
114+
}
115+
if delay > max {
116+
delay = max
117+
}
118+
d := int64(delay)
119+
// nolint:gosec // the rand package is used for fast random number generation.
120+
jitter := rand.Int63n(d) / 2
121+
return (delay * time.Duration(attempt)) + time.Duration(jitter)
105122
}
106-
d := int64(delay)
107-
// nolint:gosec // the rand package is used for fast random number generation.
108-
jitter := rand.Int63n(d) / 2
109-
return (delay * time.Duration(attempt)) + time.Duration(jitter)
110123
}

retry_example_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ func ExampleIncrementalDelay() {
137137
// Error: <nil>
138138
}
139139

140+
func ExampleIncrementalDelayMax() {
141+
// This setup will delay 20ms + 40ms + 50ms + 50ms, and a jitters at 5
142+
// attempts, until on the 6th attempt that it would succeed.
143+
l := &retry.Retry{
144+
Attempts: 6,
145+
Delay: 20 * time.Millisecond,
146+
Method: retry.IncrementalDelayMax(50 * time.Millisecond),
147+
}
148+
i := 0
149+
err := l.Do(func() error {
150+
i++
151+
if i < l.Attempts {
152+
return errors.New("ignored error")
153+
}
154+
return nil
155+
})
156+
fmt.Println("Error:", err)
157+
158+
// Output:
159+
// Error: <nil>
160+
}
161+
140162
func ExampleRetry_Do_multipleFuncs() {
141163
l := &retry.Retry{
142164
Attempts: 4,

retry_test.go

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func testLoopDoPanic(t *testing.T) {
120120
func testLoopDoSleep(t *testing.T) {
121121
t.Run("StandardMethod", testLoopDoSleepStandardMethod)
122122
t.Run("IncrementalMethod", testLoopDoSleepIncrementalMethod)
123+
t.Run("IncrementalMaxMethod", testLoopDoSleepIncrementalMaxMethod)
123124
}
124125

125126
func testLoopDoSleepStandardMethod(t *testing.T) {
@@ -179,7 +180,7 @@ func testLoopDoSleepIncrementalMethodUnderSecond(t *testing.T) {
179180
fmt.Sprintf("wanted to take more than %s, took %s", expected.Sub(started), finished.Sub(started)),
180181
)
181182
assert.True(t, finished.Before(expected.Add(6*delay)),
182-
fmt.Sprintf("take (%s) more than expected: %s", finished.Sub(started), expected.Add(6*delay)),
183+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), expected.Add(6*delay)),
183184
)
184185
}
185186

@@ -210,7 +211,7 @@ func testLoopDoSleepIncrementalMethodOverSecond(t *testing.T) {
210211
fmt.Sprintf("wanted to take more than %s, took %s", expected.Sub(started), finished.Sub(started)),
211212
)
212213
assert.True(t, finished.Before(expected.Add(2*time.Second)),
213-
fmt.Sprintf("take (%s) more than expected: %s", finished.Sub(started), 4*time.Second),
214+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), 4*time.Second),
214215
)
215216
}
216217

@@ -233,7 +234,98 @@ func testLoopDoSleepIncrementalMethodZero(t *testing.T) {
233234

234235
expected := started.Add(time.Second)
235236
assert.True(t, finished.Before(expected),
236-
fmt.Sprintf("take (%s) more than expected: %s", finished.Sub(started), time.Second),
237+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), time.Second),
238+
)
239+
}
240+
241+
func testLoopDoSleepIncrementalMaxMethod(t *testing.T) {
242+
t.Run("UnderSecond", testLoopDoSleepIncrementalMaxMethodUnderSecond)
243+
t.Run("OverSecond", testLoopDoSleepIncrementalMaxMethodOverTwoSeconds)
244+
t.Run("Zero", testLoopDoSleepIncrementalMaxMethodZero)
245+
}
246+
247+
func testLoopDoSleepIncrementalMaxMethodUnderSecond(t *testing.T) {
248+
t.Parallel()
249+
// In this setup, the delays would be (almost) 100, 200, 300, 300. So in
250+
// almost 900 ms there would be 4 calls. There is a 4*delay amount of
251+
// wiggle added.
252+
delay := 100 * time.Millisecond
253+
l := &retry.Retry{
254+
Attempts: 4,
255+
Delay: delay,
256+
Method: retry.IncrementalDelayMax(delay * 3),
257+
}
258+
259+
count := 0
260+
started := time.Now()
261+
err := l.Do(func() error {
262+
count++
263+
return assert.AnError
264+
})
265+
finished := time.Now()
266+
expected := started.Add(900 * time.Millisecond)
267+
268+
assert.Equal(t, l.Attempts, count)
269+
assert.ErrorIs(t, err, assert.AnError)
270+
assert.True(t, finished.After(expected),
271+
fmt.Sprintf("wanted to take more than %s, took %s", expected.Sub(started), finished.Sub(started)),
272+
)
273+
assert.True(t, finished.Before(expected.Add(6*delay)),
274+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), expected.Add(6*delay)),
275+
)
276+
}
277+
278+
func testLoopDoSleepIncrementalMaxMethodOverTwoSeconds(t *testing.T) {
279+
t.Parallel()
280+
if testing.Short() {
281+
t.Skip("slow test")
282+
}
283+
l := &retry.Retry{
284+
Attempts: 2,
285+
Delay: 10 * time.Second,
286+
Method: retry.IncrementalDelayMax(2 * time.Second),
287+
}
288+
289+
count := 0
290+
started := time.Now()
291+
err := l.Do(func() error {
292+
count++
293+
return assert.AnError
294+
})
295+
finished := time.Now()
296+
expected := started.Add(4 * time.Second)
297+
298+
assert.ErrorIs(t, err, assert.AnError)
299+
assert.Equal(t, l.Attempts, count)
300+
301+
assert.True(t, finished.After(expected),
302+
fmt.Sprintf("wanted to take more than %s, took %s", expected.Sub(started), finished.Sub(started)),
303+
)
304+
assert.True(t, finished.Before(expected.Add(2*2*time.Second)),
305+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), 4*time.Second),
306+
)
307+
}
308+
309+
func testLoopDoSleepIncrementalMaxMethodZero(t *testing.T) {
310+
t.Parallel()
311+
l := &retry.Retry{
312+
Attempts: 50,
313+
Method: retry.IncrementalDelayMax(time.Second / 2),
314+
}
315+
316+
count := 0
317+
started := time.Now()
318+
err := l.Do(func() error {
319+
count++
320+
return assert.AnError
321+
})
322+
finished := time.Now()
323+
assert.ErrorIs(t, err, assert.AnError)
324+
assert.Equal(t, l.Attempts, count)
325+
326+
expected := started.Add(time.Second)
327+
assert.True(t, finished.Before(expected),
328+
fmt.Sprintf("took (%s) more than expected: %s", finished.Sub(started), time.Second),
237329
)
238330
}
239331

0 commit comments

Comments
 (0)