forked from carlescere/goback
-
Notifications
You must be signed in to change notification settings - Fork 0
/
goback.go
157 lines (130 loc) · 3.99 KB
/
goback.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 goback implements a simple exponential backoff
//
// An exponential backoff approach is typically used when treating with potentially
// faulty/slow systems. If a system fails quick retries may exacerbate the system
// specially when the system is dealing with several clients. In this case a backoff
// provides the faulty system enough room to recover.
//
// Simple example:
// func main() {
// b := &goback.SimpleBackoff(
// Min: 100 * time.Millisecond,
// Max: 60 * time.Second,
// Factor: 2,
// )
// goback.Wait(b) // sleeps 100ms
// goback.Wait(b) // sleeps 200ms
// goback.Wait(b) // sleeps 400ms
// fmt.Println(b.NextRun()) // prints 800ms
// b.Reset() // resets the backoff
// goback.Wait(b) // sleeps 100ms
// }
//
// Furter examples can be found in the examples folder in the repository.
package goback
import (
"errors"
"math"
"math/rand"
"time"
)
var (
// ErrMaxAttemptsExceeded indicates that the maximum retries has been
// excedeed. Usually to consider a service unreachable/unavailable.
ErrMaxAttemptsExceeded = errors.New("maximum of attempts exceeded")
)
// Backoff is the interface that any Backoff strategy needs to implement.
type Backoff interface {
// NextAttempt returns the duration to wait for the next retry.
NextAttempt() (time.Duration, error)
// Reset clears the number of tries. Next call to NextAttempt will return
// the minimum backoff time (if there is no error).
Reset()
}
// SimpleBackoff provides a simple strategy to backoff.
type SimpleBackoff struct {
attempts uint64
next time.Duration
MaxAttempts uint64
Factor float64
Min time.Duration
Max time.Duration
}
// NextAttempt returns the duration to wait for the next retry.
func (b *SimpleBackoff) NextAttempt() (time.Duration, error) {
if b.MaxAttempts > 0 && b.attempts >= b.MaxAttempts {
return 0, ErrMaxAttemptsExceeded
}
switch {
case b.next >= b.Max:
b.next = b.Max
default:
b.next = GetNextDuration(b.Min, b.Max, b.Factor, b.attempts)
}
// We should guard against overflow in cases where MaxAttempts is not set
if b.attempts < math.MaxUint64 {
b.attempts++
}
return b.next, nil
}
// Reset clears the number of tries. Next call to NextAttempt will return
// the minimum backoff time (if there is no error).
func (b *SimpleBackoff) Reset() {
b.attempts = 0
b.next = 0
}
// JitterBackoff provides an strategy similar to SimpleBackoff but lightly randomises
// the duration to minimise collisions between contending clients.
type JitterBackoff struct {
SimpleBackoff
}
// NextAttempt returns the duration to wait for the next retry.
func (b *JitterBackoff) NextAttempt() (time.Duration, error) {
next, err := b.SimpleBackoff.NextAttempt()
if err != nil {
return 0, err
}
return addJitter(next, b.Min), nil
}
// GetNextDuration returns the duration for the strategies considering the minimum and
// maximum durations, the factor of increase and the number of attemtps tried.
func GetNextDuration(min, max time.Duration, factor float64, attempts uint64) time.Duration {
d := time.Duration(float64(min) * math.Pow(factor, float64(attempts)))
if d > max {
return max
}
return d
}
// Wait sleeps for the duration of the time specified by the backoff strategy.
func Wait(b Backoff) error {
next, err := b.NextAttempt()
if err != nil {
return err
}
time.Sleep(next)
return nil
}
// After returns a channel that will be called after the time specified by the backoff
// strategy or will exit immediately with an error.
func After(b Backoff) <-chan error {
c := make(chan error, 1)
next, err := b.NextAttempt()
if err != nil {
c <- err
close(c)
return c
}
go func() {
time.Sleep(next)
c <- nil
close(c)
}()
return c
}
// addJitter randomises the final duration
func addJitter(next, min time.Duration) time.Duration {
return time.Duration(rand.Float64()*float64(2*min) + float64(next-min))
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}