Skip to content

Commit 652117f

Browse files
committed
Switched backoff implementation from a 'Full Jitter' to a 'Ranged Jitter'
Signed-off-by: Marco Pracucci <[email protected]>
1 parent 870fe73 commit 652117f

File tree

2 files changed

+148
-15
lines changed

2 files changed

+148
-15
lines changed

pkg/util/backoff.go

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,28 @@ func (cfg *BackoffConfig) RegisterFlags(prefix string, f *flag.FlagSet) {
2424

2525
// Backoff implements exponential backoff with randomized wait times
2626
type Backoff struct {
27-
cfg BackoffConfig
28-
ctx context.Context
29-
numRetries int
30-
duration time.Duration
27+
cfg BackoffConfig
28+
ctx context.Context
29+
numRetries int
30+
nextDelayMin time.Duration
31+
nextDelayMax time.Duration
3132
}
3233

3334
// NewBackoff creates a Backoff object. Pass a Context that can also terminate the operation.
3435
func NewBackoff(ctx context.Context, cfg BackoffConfig) *Backoff {
3536
return &Backoff{
36-
cfg: cfg,
37-
ctx: ctx,
38-
duration: cfg.MinBackoff,
37+
cfg: cfg,
38+
ctx: ctx,
39+
nextDelayMin: cfg.MinBackoff,
40+
nextDelayMax: doubleDuration(cfg.MinBackoff, cfg.MaxBackoff),
3941
}
4042
}
4143

4244
// Reset the Backoff back to its initial condition
4345
func (b *Backoff) Reset() {
4446
b.numRetries = 0
45-
b.duration = b.cfg.MinBackoff
47+
b.nextDelayMin = b.cfg.MinBackoff
48+
b.nextDelayMax = doubleDuration(b.cfg.MinBackoff, b.cfg.MaxBackoff)
4649
}
4750

4851
// Ongoing returns true if caller should keep going
@@ -70,18 +73,45 @@ func (b *Backoff) NumRetries() int {
7073
// Wait sleeps for the backoff time then increases the retry count and backoff time
7174
// Returns immediately if Context is terminated
7275
func (b *Backoff) Wait() {
73-
b.numRetries++
74-
// Based on the "Full Jitter" approach from https://www.awsarchitectureblog.com/2015/03/backoff.html
75-
// sleep = random_between(0, min(cap, base * 2 ** attempt))
76+
// Increase the number of retries and get the next delay
77+
sleepTime := b.nextDelay()
78+
7679
if b.Ongoing() {
77-
sleepTime := time.Duration(rand.Int63n(int64(b.duration)))
7880
select {
7981
case <-b.ctx.Done():
8082
case <-time.After(sleepTime):
8183
}
8284
}
83-
b.duration = b.duration * 2
84-
if b.duration > b.cfg.MaxBackoff {
85-
b.duration = b.cfg.MaxBackoff
85+
}
86+
87+
func (b *Backoff) nextDelay() time.Duration {
88+
b.numRetries++
89+
90+
// Handle the edge case the min and max have the same value
91+
// (or due to some misconfig max is < min)
92+
if b.nextDelayMin >= b.nextDelayMax {
93+
return b.nextDelayMin
8694
}
95+
96+
// Add a jitter within the next exponential backoff range
97+
sleepTime := b.nextDelayMin + time.Duration(rand.Int63n(int64(b.nextDelayMax-b.nextDelayMin)))
98+
99+
// Apply the exponential backoff to calculate the next jitter
100+
// range, unless we've already reached the max
101+
if b.nextDelayMax < b.cfg.MaxBackoff {
102+
b.nextDelayMin = doubleDuration(b.nextDelayMin, b.cfg.MaxBackoff)
103+
b.nextDelayMax = doubleDuration(b.nextDelayMax, b.cfg.MaxBackoff)
104+
}
105+
106+
return sleepTime
107+
}
108+
109+
func doubleDuration(value time.Duration, max time.Duration) time.Duration {
110+
value = value * 2
111+
112+
if value <= max {
113+
return value
114+
}
115+
116+
return max
87117
}

pkg/util/backoff_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package util
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestBackoff_nextDelay(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := map[string]struct {
13+
minBackoff time.Duration
14+
maxBackoff time.Duration
15+
expectedRanges [][]time.Duration
16+
}{
17+
"exponential backoff with jitter honoring min and max": {
18+
minBackoff: 100 * time.Millisecond,
19+
maxBackoff: 10 * time.Second,
20+
expectedRanges: [][]time.Duration{
21+
{100 * time.Millisecond, 200 * time.Millisecond},
22+
{200 * time.Millisecond, 400 * time.Millisecond},
23+
{400 * time.Millisecond, 800 * time.Millisecond},
24+
{800 * time.Millisecond, 1600 * time.Millisecond},
25+
{1600 * time.Millisecond, 3200 * time.Millisecond},
26+
{3200 * time.Millisecond, 6400 * time.Millisecond},
27+
{6400 * time.Millisecond, 10000 * time.Millisecond},
28+
{6400 * time.Millisecond, 10000 * time.Millisecond},
29+
},
30+
},
31+
"exponential backoff with max equal to the end of a range": {
32+
minBackoff: 100 * time.Millisecond,
33+
maxBackoff: 800 * time.Millisecond,
34+
expectedRanges: [][]time.Duration{
35+
{100 * time.Millisecond, 200 * time.Millisecond},
36+
{200 * time.Millisecond, 400 * time.Millisecond},
37+
{400 * time.Millisecond, 800 * time.Millisecond},
38+
{400 * time.Millisecond, 800 * time.Millisecond},
39+
},
40+
},
41+
"exponential backoff with max equal to the end of a range + 1": {
42+
minBackoff: 100 * time.Millisecond,
43+
maxBackoff: 801 * time.Millisecond,
44+
expectedRanges: [][]time.Duration{
45+
{100 * time.Millisecond, 200 * time.Millisecond},
46+
{200 * time.Millisecond, 400 * time.Millisecond},
47+
{400 * time.Millisecond, 800 * time.Millisecond},
48+
{800 * time.Millisecond, 801 * time.Millisecond},
49+
{800 * time.Millisecond, 801 * time.Millisecond},
50+
},
51+
},
52+
"exponential backoff with max equal to the end of a range - 1": {
53+
minBackoff: 100 * time.Millisecond,
54+
maxBackoff: 799 * time.Millisecond,
55+
expectedRanges: [][]time.Duration{
56+
{100 * time.Millisecond, 200 * time.Millisecond},
57+
{200 * time.Millisecond, 400 * time.Millisecond},
58+
{400 * time.Millisecond, 799 * time.Millisecond},
59+
{400 * time.Millisecond, 799 * time.Millisecond},
60+
},
61+
},
62+
"min backoff is equal to max": {
63+
minBackoff: 100 * time.Millisecond,
64+
maxBackoff: 100 * time.Millisecond,
65+
expectedRanges: [][]time.Duration{
66+
{100 * time.Millisecond, 100 * time.Millisecond},
67+
{100 * time.Millisecond, 100 * time.Millisecond},
68+
{100 * time.Millisecond, 100 * time.Millisecond},
69+
},
70+
},
71+
"min backoff is greater then max": {
72+
minBackoff: 200 * time.Millisecond,
73+
maxBackoff: 100 * time.Millisecond,
74+
expectedRanges: [][]time.Duration{
75+
{200 * time.Millisecond, 200 * time.Millisecond},
76+
{200 * time.Millisecond, 200 * time.Millisecond},
77+
{200 * time.Millisecond, 200 * time.Millisecond},
78+
},
79+
},
80+
}
81+
82+
for testName, testData := range tests {
83+
testData := testData
84+
85+
t.Run(testName, func(t *testing.T) {
86+
t.Parallel()
87+
88+
b := NewBackoff(context.Background(), BackoffConfig{
89+
MinBackoff: testData.minBackoff,
90+
MaxBackoff: testData.maxBackoff,
91+
MaxRetries: len(testData.expectedRanges),
92+
})
93+
94+
for _, expectedRange := range testData.expectedRanges {
95+
delay := b.nextDelay()
96+
97+
if delay < expectedRange[0] || delay > expectedRange[1] {
98+
t.Errorf("%d expected to be within %d and %d", delay, expectedRange[0], expectedRange[1])
99+
}
100+
}
101+
})
102+
}
103+
}

0 commit comments

Comments
 (0)