Skip to content

Commit 009eed1

Browse files
authored
Handle overflow cases (#13)
1 parent 7a0bb9f commit 009eed1

File tree

5 files changed

+66
-12
lines changed

5 files changed

+66
-12
lines changed

backoff.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func WithCappedDuration(cap time.Duration, next Backoff) Backoff {
103103
return 0, true
104104
}
105105

106-
if val > cap {
106+
if val <= 0 || val > cap {
107107
val = cap
108108
}
109109
return val, false
@@ -127,7 +127,7 @@ func WithMaxDuration(timeout time.Duration, next Backoff) Backoff {
127127
return 0, true
128128
}
129129

130-
if val > diff {
130+
if val <= 0 || val > diff {
131131
val = diff
132132
}
133133
return val, false

backoff_exponential.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package retry
22

33
import (
44
"context"
5+
"math"
56
"sync/atomic"
67
"time"
78
)
@@ -11,17 +12,19 @@ type exponentialBackoff struct {
1112
attempt uint64
1213
}
1314

14-
// Exponential is a wrapper around Retry that uses an exponential backoff. It's
15-
// very efficient, but does not check for overflow, so ensure you bound the
16-
// retry. It panics if the given base is less than zero.
15+
// Exponential is a wrapper around Retry that uses an exponential backoff. See
16+
// NewExponential.
1717
func Exponential(ctx context.Context, base time.Duration, f RetryFunc) error {
1818
return Do(ctx, NewExponential(base), f)
1919
}
2020

2121
// NewExponential creates a new exponential backoff using the starting value of
2222
// base and doubling on each failure (1, 2, 4, 8, 16, 32, 64...), up to max.
23-
// It's very efficient, but does not check for overflow, so ensure you bound the
24-
// retry. It panics if the given base is less than 0.
23+
//
24+
// Once it overflows, the function constantly returns the maximum time.Duration
25+
// for a 64-bit integer.
26+
//
27+
// It panics if the given base is less than zero.
2528
func NewExponential(base time.Duration) Backoff {
2629
if base <= 0 {
2730
panic("base must be greater than 0")
@@ -34,5 +37,11 @@ func NewExponential(base time.Duration) Backoff {
3437

3538
// Next implements Backoff. It is safe for concurrent use.
3639
func (b *exponentialBackoff) Next() (time.Duration, bool) {
37-
return b.base << (atomic.AddUint64(&b.attempt, 1) - 1), false
40+
next := b.base << (atomic.AddUint64(&b.attempt, 1) - 1)
41+
if next <= 0 {
42+
atomic.AddUint64(&b.attempt, ^uint64(0))
43+
next = math.MaxInt64
44+
}
45+
46+
return next, false
3847
}

backoff_exponential_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package retry_test
22

33
import (
44
"fmt"
5+
"math"
56
"reflect"
67
"sort"
78
"testing"
@@ -48,6 +49,23 @@ func TestExponentialBackoff(t *testing.T) {
4849
8192 * time.Nanosecond,
4950
},
5051
},
52+
{
53+
name: "overflow",
54+
base: 100_000 * time.Hour,
55+
tries: 10,
56+
exp: []time.Duration{
57+
100_000 * time.Hour,
58+
200_000 * time.Hour,
59+
400_000 * time.Hour,
60+
800_000 * time.Hour,
61+
1_600_000 * time.Hour,
62+
math.MaxInt64,
63+
math.MaxInt64,
64+
math.MaxInt64,
65+
math.MaxInt64,
66+
math.MaxInt64,
67+
},
68+
},
5169
}
5270

5371
for _, tc := range cases {

backoff_fibonacci.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package retry
22

33
import (
44
"context"
5+
"math"
56
"sync/atomic"
67
"time"
78
"unsafe"
@@ -13,16 +14,20 @@ type fibonacciBackoff struct {
1314
state unsafe.Pointer
1415
}
1516

16-
// Fibonacci is a wrapper around Retry that uses a Fibonacci backoff. It panics
17-
// if the given base is less than zero.
17+
// Fibonacci is a wrapper around Retry that uses a Fibonacci backoff. See
18+
// NewFibonacci.
1819
func Fibonacci(ctx context.Context, base time.Duration, f RetryFunc) error {
1920
return Do(ctx, NewFibonacci(base), f)
2021
}
2122

2223
// NewFibonacci creates a new Fibonacci backoff using the starting value of
2324
// base. The wait time is the sum of the previous two wait times on each failed
24-
// attempt (1, 1, 2, 3, 5, 8, 13...). It panics if the given base is less than
25-
// zero.
25+
// attempt (1, 1, 2, 3, 5, 8, 13...).
26+
//
27+
// Once it overflows, the function constantly returns the maximum time.Duration
28+
// for a 64-bit integer.
29+
//
30+
// It panics if the given base is less than zero.
2631
func NewFibonacci(base time.Duration) Backoff {
2732
if base <= 0 {
2833
panic("base must be greater than 0")
@@ -40,6 +45,10 @@ func (b *fibonacciBackoff) Next() (time.Duration, bool) {
4045
currState := (*state)(curr)
4146
next := currState[0] + currState[1]
4247

48+
if next <= 0 {
49+
return math.MaxInt64, false
50+
}
51+
4352
if atomic.CompareAndSwapPointer(&b.state, curr, unsafe.Pointer(&state{currState[1], next})) {
4453
return next, false
4554
}

backoff_fibonacci_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package retry_test
22

33
import (
44
"fmt"
5+
"math"
56
"reflect"
67
"sort"
78
"testing"
@@ -60,6 +61,23 @@ func TestFibonacciBackoff(t *testing.T) {
6061
610 * time.Nanosecond,
6162
},
6263
},
64+
{
65+
name: "overflow",
66+
base: 100_000 * time.Hour,
67+
tries: 10,
68+
exp: []time.Duration{
69+
100_000 * time.Hour,
70+
200_000 * time.Hour,
71+
300_000 * time.Hour,
72+
500_000 * time.Hour,
73+
800_000 * time.Hour,
74+
1_300_000 * time.Hour,
75+
2_100_000 * time.Hour,
76+
math.MaxInt64,
77+
math.MaxInt64,
78+
math.MaxInt64,
79+
},
80+
},
6381
}
6482

6583
for _, tc := range cases {

0 commit comments

Comments
 (0)