Skip to content

Commit 19ec4bf

Browse files
authored
fix(retry): clamp negative attempt to 0 in BackoffDuration
A negative attempt value caused a runtime panic because Go does not allow negative bit-shift amounts. Added a shift < 0 clamp so negative attempts return initial (attempt-0 behavior). Updated doc comment. Closes #812
1 parent d5fc7c4 commit 19ec4bf

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

sdk/retry/retry.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ func RunDo(ctx context.Context, enabled bool, cfg Config, fn func(ctx context.Co
134134
//
135135
// The base duration for attempt n is initial * 2^n, capped at max.
136136
//
137+
// Negative attempt values are treated as 0: the function returns initial (or
138+
// its jittered equivalent) without panicking.
139+
//
137140
// When jitter is true, the result is a uniform random value in [0, backoff)
138141
// — full jitter with no minimum floor. A 5 s capped backoff can therefore
139142
// jitter down to near-zero on any given attempt. If a guaranteed minimum wait
@@ -147,6 +150,9 @@ func RunDo(ctx context.Context, enabled bool, cfg Config, fn func(ctx context.Co
147150
// without performing the shift.
148151
func BackoffDuration(attempt int, initial, max time.Duration, jitter bool) time.Duration {
149152
shift := attempt
153+
if shift < 0 {
154+
shift = 0
155+
}
150156
if shift > 62 {
151157
shift = 62
152158
}

sdk/retry/retry_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ func TestConfig_WithDefaults(t *testing.T) {
5151
}
5252
}
5353

54+
// TestBackoffDuration_NegativeAttempt verifies that negative attempt values do
55+
// not panic and are treated as attempt 0 (returning initial).
56+
func TestBackoffDuration_NegativeAttempt(t *testing.T) {
57+
initial := 100 * time.Millisecond
58+
max := time.Second
59+
60+
cases := []int{-1, -99}
61+
for _, attempt := range cases {
62+
got := BackoffDuration(attempt, initial, max, false)
63+
if got != initial {
64+
t.Errorf("attempt %d: got %v, want %v", attempt, got, initial)
65+
}
66+
}
67+
}
68+
5469
func TestBackoffDuration_NormalProgression(t *testing.T) {
5570
initial := 100 * time.Millisecond
5671
max := 5 * time.Second
@@ -95,7 +110,7 @@ func TestBackoffDuration_JitterBound(t *testing.T) {
95110

96111
for attempt := 0; attempt <= 10; attempt++ {
97112
noJitter := BackoffDuration(attempt, initial, max, false)
98-
for i := 0; i < 50; i++ {
113+
for i := range 50 {
99114
got := BackoffDuration(attempt, initial, max, true)
100115
if got < 0 || (noJitter > 0 && got >= noJitter) {
101116
t.Errorf("attempt %d iter %d: jitter result %v out of [0, %v)", attempt, i, got, noJitter)
@@ -114,7 +129,7 @@ func TestBackoffDuration_HighAttemptsJitter(t *testing.T) {
114129
// What we can assert is that it is non-negative (no wrap-around to negative)
115130
// and strictly less than max (jitter always reduces).
116131
for attempt := 56; attempt <= 62; attempt++ {
117-
for i := 0; i < 20; i++ {
132+
for range 20 {
118133
got := BackoffDuration(attempt, initial, max, true)
119134
if got < 0 {
120135
t.Errorf("attempt %d: jitter produced negative duration %v", attempt, got)

0 commit comments

Comments
 (0)