Skip to content

Commit 24bacbb

Browse files
committed
minor changes
1 parent 8726798 commit 24bacbb

4 files changed

Lines changed: 87 additions & 32 deletions

File tree

retry/EXAMPLES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828
```
2929
#### Output:
3030
```
31-
"response" <nil>
31+
response <nil>
3232
```
3333

3434
---
@@ -140,7 +140,7 @@ attempt 3 fixed=1s linear=3s exponential=8s
140140

141141
---
142142

143-
### Respecting context cancellation
143+
### Respecting the total timeout
144144
```go
145145
package main
146146

retry/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ The `retry` package provides a generic, context-aware way to retry fallible oper
44

55
#### **Options**
66

7-
- **`MaxAttempts int`**: Total number of times the operation will be tried before giving up.
7+
- **`MaxAttempts uint`**: Total number of times the operation will be tried before giving up.
88
- **`TotalTimeout time.Duration`**: Maximum time allowed across all attempts, including backoff delays.
9-
- **`Backoff func(attempt int) time.Duration`**: Returns how long to wait before the next attempt. `attempt` is zero-indexed.
9+
- **`Backoff func(attempt uint) time.Duration`**: Returns how long to wait before the next attempt. `attempt` is zero-indexed.
1010
- **`ShouldRetry func(err error) bool`**: Reports whether the given error is retryable. Return `false` to abort immediately.
1111

1212
#### **Functions**
@@ -19,14 +19,14 @@ The `retry` package provides a generic, context-aware way to retry fallible oper
1919

2020
#### **Backoff Strategies**
2121

22-
- **`FixedBackoff(d time.Duration) func(attempt int) time.Duration`**:
22+
- **`FixedBackoff(d time.Duration) func(attempt uint) time.Duration`**:
2323
Waits exactly `d` between every attempt.
2424

25-
- **`LinearBackoff(d time.Duration) func(attempt int) time.Duration`**:
25+
- **`LinearBackoff(d time.Duration) func(attempt uint) time.Duration`**:
2626
Waits `d * attempt`. Grows linearly: `0, d, 2d, 3d, …`
2727

28-
- **`ExponentialBackoff(d time.Duration) func(attempt int) time.Duration`**:
29-
Waits `d * 2^attempt`. Doubles on each failure: `d, 2d, 4d, 8d, …`
28+
- **`ExponentialBackoff(d time.Duration) func(attempt uint) time.Duration`**:
29+
Waits `max(d * 2^attempt, 2^63 - 1)`. Doubles on each failure: `d, 2d, 4d, 8d, …`
3030

3131
#### **Notes**
3232
- `TotalTimeout` is enforced via a derived context passed to every attempt. If the deadline is exceeded mid-backoff, `Do` returns `context.DeadlineExceeded` immediately.

retry/retry.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ package retry
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"time"
78
)
89

910
type Options struct {
10-
MaxAttempts int
11+
MaxAttempts uint
1112
TotalTimeout time.Duration
1213
// Backoff returns how long to wait before the next attempt.
1314
// attempt is zero-indexed (0 = pause after the first failure).
14-
Backoff func(attempt int) time.Duration
15+
Backoff func(attempt uint) time.Duration
1516

1617
// ShouldRetry reports whether the given error is retryable.
1718
// Return false to abort immediately.
@@ -23,10 +24,14 @@ type RetryFunc[T any] func(ctx context.Context) (T, error)
2324
// Do calls fn repeatedly until fn succeeds, ShouldRetry returns false,
2425
// MaxAttempts is reached, or TotalTimeout elapses.
2526
func Do[T any](ctx context.Context, opts Options, fn RetryFunc[T]) (T, error) {
27+
var zero T
28+
if ctx == nil {
29+
return zero, fmt.Errorf("retry: context must not be nil")
30+
}
31+
2632
ctx, cls := context.WithTimeout(ctx, opts.TotalTimeout)
2733
defer cls()
2834

29-
var zero T
3035
for attempt := range opts.MaxAttempts {
3136
ret, err := fn(ctx)
3237
if err == nil {
@@ -58,22 +63,40 @@ func DoVoid(ctx context.Context, opts Options, fn func(ctx context.Context) erro
5863
}
5964

6065
// FixedBackoff returns a backoff that always waits exactly d.
61-
func FixedBackoff(d time.Duration) func(attempt int) time.Duration {
62-
return func(attempt int) time.Duration {
66+
func FixedBackoff(d time.Duration) func(attempt uint) time.Duration {
67+
return func(attempt uint) time.Duration {
6368
return d
6469
}
6570
}
6671

6772
// LinearBackoff returns a backoff that waits d * attempt.
68-
func LinearBackoff(d time.Duration) func(attempt int) time.Duration {
69-
return func(attempt int) time.Duration {
73+
func LinearBackoff(d time.Duration) func(attempt uint) time.Duration {
74+
return func(attempt uint) time.Duration {
75+
if attempt > math.MaxInt64 {
76+
return math.MaxInt64
77+
}
78+
7079
return d * time.Duration(attempt)
7180
}
7281
}
7382

74-
// ExponentialBackoff returns a backoff that waits d * 2^attempt.
75-
func ExponentialBackoff(d time.Duration) func(attempt int) time.Duration {
76-
return func(attempt int) time.Duration {
77-
return d * time.Duration(1<<attempt)
83+
// ExponentialBackoff returns a backoff that waits max(d * 2^attempt, 2^63 - 1).
84+
func ExponentialBackoff(d time.Duration) func(attempt uint) time.Duration {
85+
return func(attempt uint) time.Duration {
86+
maxDuration := time.Duration(1<<63 - 1)
87+
if attempt <= 0 || d < 0 {
88+
return d
89+
}
90+
91+
if attempt >= 63 {
92+
return maxDuration
93+
}
94+
95+
multiplier := time.Duration(1) << attempt
96+
if d > maxDuration/multiplier {
97+
return maxDuration
98+
}
99+
100+
return d * multiplier
78101
}
79102
}

retry/retry_test.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,16 @@ func TestDo_ParentContextCancelled(t *testing.T) {
131131
}
132132

133133
func TestDo_ZeroValueReturnedOnError(t *testing.T) {
134-
_, err := Do(context.Background(), defaultOpts(), func(ctx context.Context) (string, error) {
134+
result, err := Do(context.Background(), defaultOpts(), func(ctx context.Context) (string, error) {
135135
return "partial", errors.New("fail")
136136
})
137137
if err == nil {
138138
t.Fatal("expected error")
139139
}
140+
141+
if result != "" {
142+
t.Fatalf("expected zero value, got %v", result)
143+
}
140144
}
141145

142146
func TestDoVoid_Success(t *testing.T) {
@@ -169,7 +173,7 @@ func TestDoVoid_RetriesAndFails(t *testing.T) {
169173

170174
func TestFixedBackoff(t *testing.T) {
171175
b := FixedBackoff(100 * time.Millisecond)
172-
for _, attempt := range []int{0, 1, 2, 5} {
176+
for _, attempt := range []uint{0, 1, 2, 5} {
173177
if got := b(attempt); got != 100*time.Millisecond {
174178
t.Fatalf("attempt %d: expected 100ms, got %v", attempt, got)
175179
}
@@ -178,7 +182,7 @@ func TestFixedBackoff(t *testing.T) {
178182

179183
func TestLinearBackoff(t *testing.T) {
180184
b := LinearBackoff(100 * time.Millisecond)
181-
cases := map[int]time.Duration{
185+
cases := map[uint]time.Duration{
182186
0: 0,
183187
1: 100 * time.Millisecond,
184188
2: 200 * time.Millisecond,
@@ -193,7 +197,7 @@ func TestLinearBackoff(t *testing.T) {
193197

194198
func TestExponentialBackoff(t *testing.T) {
195199
b := ExponentialBackoff(100 * time.Millisecond)
196-
cases := map[int]time.Duration{
200+
cases := map[uint]time.Duration{
197201
0: 100 * time.Millisecond,
198202
1: 200 * time.Millisecond,
199203
2: 400 * time.Millisecond,
@@ -216,9 +220,13 @@ func BenchmarkDo_AlwaysSucceeds(b *testing.B) {
216220

217221
b.ResetTimer()
218222
for b.Loop() {
219-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
223+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
220224
return "ok", nil
221225
})
226+
227+
if err != nil {
228+
b.Fatal(err)
229+
}
222230
}
223231
}
224232

@@ -232,9 +240,13 @@ func BenchmarkDo_AlwaysFails(b *testing.B) {
232240

233241
b.ResetTimer()
234242
for b.Loop() {
235-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
243+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
236244
return "", errors.New("fail")
237245
})
246+
247+
if err == nil {
248+
b.Fatal("expected error, got nil")
249+
}
238250
}
239251
}
240252

@@ -249,13 +261,17 @@ func BenchmarkDo_SuccessOnLastAttempt(b *testing.B) {
249261
b.ResetTimer()
250262
for b.Loop() {
251263
attempt := 0
252-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
264+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
253265
attempt++
254266
if attempt < 5 {
255267
return "", errors.New("transient")
256268
}
257269
return "ok", nil
258270
})
271+
272+
if err != nil {
273+
b.Fatal(err)
274+
}
259275
}
260276
}
261277

@@ -270,9 +286,13 @@ func BenchmarkDo_ShouldRetryFalse(b *testing.B) {
270286

271287
b.ResetTimer()
272288
for b.Loop() {
273-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
289+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
274290
return "", permanent
275291
})
292+
293+
if !errors.Is(err, permanent) {
294+
b.Fatalf("expected permanent error, got %v", err)
295+
}
276296
}
277297
}
278298

@@ -286,9 +306,13 @@ func BenchmarkDoVoid_AlwaysSucceeds(b *testing.B) {
286306

287307
b.ResetTimer()
288308
for b.Loop() {
289-
DoVoid(context.Background(), opts, func(ctx context.Context) error {
309+
err := DoVoid(context.Background(), opts, func(ctx context.Context) error {
290310
return nil
291311
})
312+
313+
if err != nil {
314+
b.Fatal(err)
315+
}
292316
}
293317
}
294318

@@ -317,7 +341,7 @@ func BenchmarkExponentialBackoff(b *testing.B) {
317341
}
318342

319343
func BenchmarkDo_MaxAttempts(b *testing.B) {
320-
for _, maxAttempts := range []int{1, 5, 10, 50} {
344+
for _, maxAttempts := range []uint{1, 5, 10, 50} {
321345
b.Run(fmt.Sprintf("attempts=%d", maxAttempts), func(b *testing.B) {
322346
opts := Options{
323347
MaxAttempts: maxAttempts,
@@ -328,16 +352,20 @@ func BenchmarkDo_MaxAttempts(b *testing.B) {
328352

329353
b.ResetTimer()
330354
for b.Loop() {
331-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
355+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
332356
return "", errors.New("fail")
333357
})
358+
359+
if err == nil {
360+
b.Fatal("expected error, got nil")
361+
}
334362
}
335363
})
336364
}
337365
}
338366

339367
func BenchmarkDo_BackoffStrategies(b *testing.B) {
340-
strategies := map[string]func(int) time.Duration{
368+
strategies := map[string]func(uint) time.Duration{
341369
"fixed": FixedBackoff(0),
342370
"linear": LinearBackoff(0),
343371
"exponential": ExponentialBackoff(0),
@@ -354,9 +382,13 @@ func BenchmarkDo_BackoffStrategies(b *testing.B) {
354382

355383
b.ResetTimer()
356384
for b.Loop() {
357-
Do(context.Background(), opts, func(ctx context.Context) (string, error) {
385+
_, err := Do(context.Background(), opts, func(ctx context.Context) (string, error) {
358386
return "", errors.New("fail")
359387
})
388+
389+
if err == nil {
390+
b.Fatal("expected error, got nil")
391+
}
360392
}
361393
})
362394
}

0 commit comments

Comments
 (0)