-
Notifications
You must be signed in to change notification settings - Fork 0
/
retry.go
151 lines (127 loc) · 3.5 KB
/
retry.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Package retry offers feature to retry a function.
// It offers different backoff strategies.
//
// Retry means when the first function execution fails, then it will do retry.
// It will try to retry until maximum allowed attempt.
// There will be a delay (backoff) from one attempt to another.
//
// When a function return unretryable error, it will not try to the next attempt.
package retry
import (
"context"
"errors"
"fmt"
"time"
)
// DefaultRetry default configuration for retrier.
const (
DefaultRetryMaxAttempts uint = 10
)
var defaultRetryBackoff = NewBackoffConstant(DefaultBackoffBaseInterval)
// Retrier denotes a retrier struct.
type Retrier struct {
maximumRetryAttempts uint
backoff Backoff
}
// UnretryableError denotes an error that should be exit immediately.
type UnretryableError struct {
error
}
func (u *UnretryableError) Unwrap() error {
return u.error
}
// Unretryable wraps an error in UnretryableError struct
// When you do this inside your function, it will not retry into the next attempt.
func Unretryable(err error) error {
return &UnretryableError{
fmt.Errorf("unretryable error: %w", err),
}
}
// ContextDoneError denotes an error that triggered when context is done.
type ContextDoneError struct {
error
}
func (u *ContextDoneError) Unwrap() error {
return u.error
}
func contextDone(err error) error {
return &ContextDoneError{
fmt.Errorf("context done error: %w", err),
}
}
// NewRetrier initializes Retrier struct.
// This is handy if you want to re-use retrier with the same configuration.
//
// If option is not provided, it will use default configuration.
// Example:
// retrier := retry.NewRetrier(
// retry.WithMaxRetryAttempts(1),
// retry.WithBackoff(
// retry.NewBackoffTruncatedExponential(
// time.Second,
// time.Millisecond,
// retry.NewRand(time.Now().UnixNano()),
// ),
// ),
// )
func NewRetrier(options ...Option) *Retrier {
rr := &Retrier{
maximumRetryAttempts: DefaultRetryMaxAttempts,
backoff: defaultRetryBackoff,
}
for _, option := range options {
option(rr)
}
return rr
}
// Do runs f with retry.
func (r *Retrier) Do(f func() error) error {
return r.do(context.Background(), f)
}
// DoWithContext runs f with retry.
// While waiting for next attempt, it will also check context cancelation.
func (r *Retrier) DoWithContext(ctx context.Context, f func() error) error {
return r.do(ctx, f)
}
// Do runs f with retry.
// This is handy if you do not want to initialize Retrier.
func Do(f func() error, options ...Option) error {
r := NewRetrier(options...)
return r.do(context.Background(), f)
}
// DoWithContext runs f with retry.
// While waiting for next attempt, it will also check context cancelation.
// This is handy if you do not want to initialize Retrier.
func DoWithContext(ctx context.Context, f func() error, options ...Option) error {
r := NewRetrier(options...)
return r.do(ctx, f)
}
func (r *Retrier) do(ctx context.Context, f func() error) error {
var attempt uint
for {
err := f()
if err == nil {
return nil
}
if !r.isRetryable(attempt, err) {
return err
}
backoffDuration := r.backoff.Get(attempt)
timer := time.NewTimer(backoffDuration)
select {
case <-ctx.Done():
timer.Stop()
return contextDone(err)
case <-timer.C:
timer.Stop()
}
attempt++
}
}
func (r *Retrier) isRetryable(attempt uint, err error) bool {
if attempt >= r.maximumRetryAttempts {
return false
}
var unretryableError *UnretryableError
return !errors.As(err, &unretryableError)
}