Skip to content

Commit 8d16616

Browse files
authored
Merge pull request avast#96 from willdot/handle-context-timeout
Allow last error to be returned with context error
2 parents fdadb7c + b94b74c commit 8d16616

File tree

4 files changed

+104
-11
lines changed

4 files changed

+104
-11
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,30 @@ example of augmenting time.After with a print statement
401401
retry.WithTimer(&MyTimer{})
402402
)
403403
404+
#### func WrapContextErrorWithLastError
405+
406+
```go
407+
func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option
408+
```
409+
WrapContextErrorWithLastError allows the context error to be returned wrapped
410+
with the last error that the retried function returned. This is only applicable
411+
when Attempts is set to 0 to retry indefinitly and when using a context to
412+
cancel / timeout
413+
414+
default is false
415+
416+
ctx, cancel := context.WithCancel(context.Background())
417+
defer cancel()
418+
419+
retry.Do(
420+
func() error {
421+
...
422+
},
423+
retry.Context(ctx),
424+
retry.Attempts(0),
425+
retry.WrapContextErrorWithLastError(true),
426+
)
427+
404428
#### type RetryIfFunc
405429
406430
```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
}
@@ -248,3 +249,26 @@ func WithTimer(t Timer) Option {
248249
c.timer = t
249250
}
250251
}
252+
253+
// WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the
254+
// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when
255+
// using a context to cancel / timeout
256+
//
257+
// default is false
258+
//
259+
// ctx, cancel := context.WithCancel(context.Background())
260+
// defer cancel()
261+
//
262+
// retry.Do(
263+
// func() error {
264+
// ...
265+
// },
266+
// retry.Context(ctx),
267+
// retry.Attempts(0),
268+
// retry.WrapContextErrorWithLastError(true),
269+
// )
270+
func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option {
271+
return func(c *Config) {
272+
c.wrapContextErrorWithLastError = wrapContextErrorWithLastError
273+
}
274+
}

retry.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (
136136
}
137137

138138
// Setting attempts to 0 means we'll retry until we succeed
139+
var lastErr error
139140
if config.attempts == 0 {
140141
for {
141142
t, err := retryableFunc()
@@ -151,11 +152,16 @@ func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (
151152
return emptyT, err
152153
}
153154

155+
lastErr = err
156+
154157
n++
155158
config.onRetry(n, err)
156159
select {
157160
case <-config.timer.After(delay(config, n, err)):
158161
case <-config.context.Done():
162+
if config.wrapContextErrorWithLastError {
163+
return emptyT, Error{config.context.Err(), lastErr}
164+
}
159165
return emptyT, config.context.Err()
160166
}
161167
}

retry_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,45 @@ func TestContext(t *testing.T) {
464464
assert.Equal(t, 2, retrySum, "called at most once")
465465
}()
466466
})
467+
468+
t.Run("cancelled on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) {
469+
ctx, cancel := context.WithCancel(context.Background())
470+
defer cancel()
471+
472+
retrySum := 0
473+
err := Do(
474+
func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} },
475+
OnRetry(func(n uint, err error) {
476+
retrySum += 1
477+
if retrySum == 2 {
478+
cancel()
479+
}
480+
}),
481+
Context(ctx),
482+
Attempts(0),
483+
WrapContextErrorWithLastError(true),
484+
)
485+
assert.ErrorIs(t, err, context.Canceled)
486+
assert.ErrorIs(t, err, fooErr{str: "error 2"})
487+
})
488+
489+
t.Run("timed out on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) {
490+
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
491+
defer cancel()
492+
493+
retrySum := 0
494+
err := Do(
495+
func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} },
496+
OnRetry(func(n uint, err error) {
497+
retrySum += 1
498+
}),
499+
Context(ctx),
500+
Attempts(0),
501+
WrapContextErrorWithLastError(true),
502+
)
503+
assert.ErrorIs(t, err, context.DeadlineExceeded)
504+
assert.ErrorIs(t, err, fooErr{str: "error 2"})
505+
})
467506
}
468507

469508
type testTimer struct {

0 commit comments

Comments
 (0)