Skip to content

Commit 27bc461

Browse files
committed
Allow last error to be returned with context error
1 parent a9a7017 commit 27bc461

File tree

4 files changed

+105
-12
lines changed

4 files changed

+105
-12
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,30 @@ retry.Do(
369369
370370
)
371371
372+
#### func WrapContextErrorWithLastError
373+
374+
```go
375+
func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option
376+
```
377+
WrapContextErrorWithLastError allows the context error to be returned wrapped
378+
with the last error that the retried function returned. This is only applicable
379+
when Attempts is set to 0 to retry indefinitly and when using a context to
380+
cancel / timeout
381+
382+
default is false
383+
384+
ctx, cancel := context.WithCancel(context.Background())
385+
defer cancel()
386+
387+
retry.Do(
388+
func() error {
389+
...
390+
},
391+
retry.Context(ctx),
392+
retry.Attempts(0),
393+
retry.WrapContextErrorWithLastError(true),
394+
)
395+
372396
#### type RetryIfFunc
373397
374398
```go

options.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ type Timer interface {
2323
}
2424

2525
type Config struct {
26-
attempts uint
27-
attemptsForError map[error]uint
28-
delay time.Duration
29-
maxDelay time.Duration
30-
maxJitter time.Duration
31-
onRetry OnRetryFunc
32-
retryIf RetryIfFunc
33-
delayType DelayTypeFunc
34-
lastErrorOnly bool
35-
context context.Context
36-
timer Timer
26+
attempts uint
27+
attemptsForError map[error]uint
28+
delay time.Duration
29+
maxDelay time.Duration
30+
maxJitter time.Duration
31+
onRetry OnRetryFunc
32+
retryIf RetryIfFunc
33+
delayType DelayTypeFunc
34+
lastErrorOnly bool
35+
context context.Context
36+
timer Timer
37+
wrapContextErrorWithLastError bool
3738

3839
maxBackOffN uint
3940
}
@@ -250,3 +251,26 @@ func WithTimer(t Timer) Option {
250251
c.timer = t
251252
}
252253
}
254+
255+
// WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the
256+
// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when
257+
// using a context to cancel / timeout
258+
//
259+
// default is false
260+
//
261+
// ctx, cancel := context.WithCancel(context.Background())
262+
// defer cancel()
263+
//
264+
// retry.Do(
265+
// func() error {
266+
// ...
267+
// },
268+
// retry.Context(ctx),
269+
// retry.Attempts(0),
270+
// retry.WrapContextErrorWithLastError(true),
271+
// )
272+
func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option {
273+
return func(c *Config) {
274+
c.wrapContextErrorWithLastError = wrapContextErrorWithLastError
275+
}
276+
}

retry.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
9595
}
9696

9797
// Setting attempts to 0 means we'll retry until we succeed
98+
var lastErr error
9899
if config.attempts == 0 {
99100
for err := retryableFunc(); err != nil; err = retryableFunc() {
100101
if !IsRecoverable(err) {
@@ -105,12 +106,17 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
105106
return err
106107
}
107108

109+
lastErr = err
110+
108111
n++
109112
config.onRetry(n, err)
110113
select {
111114
case <-config.timer.After(delay(config, n, err)):
112115
case <-config.context.Done():
113-
return config.context.Err()
116+
if !config.wrapContextErrorWithLastError {
117+
return config.context.Err()
118+
}
119+
return fmt.Errorf("%w: %w", config.context.Err(), lastErr)
114120
}
115121
}
116122

retry_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,45 @@ func TestContext(t *testing.T) {
451451
assert.Equal(t, 2, retrySum, "called at most once")
452452
}()
453453
})
454+
455+
t.Run("cancelled on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) {
456+
ctx, cancel := context.WithCancel(context.Background())
457+
defer cancel()
458+
459+
retrySum := 0
460+
err := Do(
461+
func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} },
462+
OnRetry(func(n uint, err error) {
463+
retrySum += 1
464+
if retrySum == 2 {
465+
cancel()
466+
}
467+
}),
468+
Context(ctx),
469+
Attempts(0),
470+
WrapContextErrorWithLastError(true),
471+
)
472+
assert.ErrorIs(t, err, context.Canceled)
473+
assert.ErrorIs(t, err, fooErr{str: "error 2"})
474+
})
475+
476+
t.Run("timed out on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) {
477+
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
478+
defer cancel()
479+
480+
retrySum := 0
481+
err := Do(
482+
func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} },
483+
OnRetry(func(n uint, err error) {
484+
retrySum += 1
485+
}),
486+
Context(ctx),
487+
Attempts(0),
488+
WrapContextErrorWithLastError(true),
489+
)
490+
assert.ErrorIs(t, err, context.DeadlineExceeded)
491+
assert.ErrorIs(t, err, fooErr{str: "error 2"})
492+
})
454493
}
455494

456495
type testTimer struct {

0 commit comments

Comments
 (0)