-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathriprovare.go
157 lines (140 loc) · 4.16 KB
/
riprovare.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
152
153
154
155
156
157
package riprovare
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Retryable is a function that can be retried. If a non-nil error value is
// returned the function will be retried based on the RetryPolicy.
type Retryable func() error
// RetryPolicy is function type that returns a boolean indicating if operations
// should continue retrying. An error is accepted that allows for the error value
// to be inspected. Optionally retries can be abandoned or continue depending on
// the error value.
type RetryPolicy func(error) bool
// OnErrorFunc is a function type that is invoked when an error occurs which provides
// a hook to log errors, capture metrics, etc.
type OnErrorFunc func(error)
// SimpleRetryPolicy is a RetryPolicy that retries the max attempts with no delay
// between retries.
func SimpleRetryPolicy(attempts int) RetryPolicy {
return func(err error) bool {
// If the error is from the context being canceled there is no reason
// to continue retrying
if errors.Is(err, context.Canceled) {
return false
}
if attempts--; attempts > 0 {
return true
}
return false
}
}
// FixedRetryPolicy returns a RetryPolicy that retries the max attempts delaying
// the provided fixed duration between attempts.
func FixedRetryPolicy(attempts int, delay time.Duration) RetryPolicy {
return func(err error) bool {
// If the error is from the context being canceled there is no reason
// to continue retrying
if errors.Is(err, context.Canceled) {
return false
}
if attempts--; attempts > 0 {
time.Sleep(delay)
return true
}
return false
}
}
// ExponentialBackoffRetryPolicy is a RetryPolicy that retries the max attempts
// with a delay between each retry. After each attempt the delay duration is doubled
// +/- 25% jitter.
func ExponentialBackoffRetryPolicy(attempts int, initialDelay time.Duration) RetryPolicy {
delay := initialDelay
return func(err error) bool {
// If the error is from the context being canceled there is no reason
// to continue retrying
if errors.Is(err, context.Canceled) {
return false
}
if attempts--; attempts > 0 {
time.Sleep(delay)
delay = exponential(delay)
return true
}
return false
}
}
// Option allows additional configuration of the retries.
type Option func(r *retry)
// ErrorHook adds a callback when an error occurs but before the next retry.
// This allows for the user of this package to capture errors or logging,
// metrics, etc.
func ErrorHook(fn OnErrorFunc) Option {
// Protect against illegal use of API, if someone does this all hope is lost.
// Technically letting this pass wouldn't cause a panic at runtime because the
// OnErrorFunc is only invoked if it is non-nil, but passing the ErrorHook option
// to Retry with a nil can be nothing but a programmer error because well ... it
// makes no sense.
if fn == nil {
panic(fmt.Errorf("illegal use of api, cannot invoke a nil function"))
}
return func(r *retry) {
r.onError = fn
}
}
// Retry invokes a Retryable and retries according to the provided RetryPolicy.
// Once all attempts have been exhausted this function will return an
// UnrecoverableError.
//
// A zero-value/nil RetryPolicy or Retryable will cause a panic.
func Retry(policy RetryPolicy, fn Retryable, opts ...Option) error {
if policy == nil {
panic(fmt.Errorf("illegal use of api: cannot operate on nil RetryPolicy"))
}
if fn == nil {
panic(fmt.Errorf("illegal use of api: cannot invoke nil function"))
}
r := &retry{
fn: fn,
policy: policy,
}
for _, opt := range opts {
opt(r)
}
return r.do()
}
type retry struct {
policy RetryPolicy
fn Retryable
onError OnErrorFunc
}
func (r retry) do() error {
if err := r.fn(); err != nil {
if r.onError != nil {
r.onError(err)
}
if r.policy(err) {
return r.do()
}
return UnrecoverableError{Err: err}
}
return nil
}
type UnrecoverableError struct {
Err error
}
func (u UnrecoverableError) Error() string {
return fmt.Sprintf("max retries exceeded: %s", u.Err)
}
func exponential(d time.Duration) time.Duration {
d *= 2
jitter := rand.Float64() + 0.25
d = time.Duration(int64(float64(d.Nanoseconds()) * jitter))
return d
}