File: backoff.go

package info (click to toggle)
golang-github-cloudflare-cfssl 1.2.0%2Bgit20160825.89.7fb22c8-3
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 4,916 kB
  • ctags: 2,827
  • sloc: sh: 146; sql: 62; python: 11; makefile: 8
file content (198 lines) | stat: -rw-r--r-- 4,621 bytes parent folder | download | duplicates (2)
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package core

// Package backoff contains an implementation of an intelligent backoff
// strategy. It is based on the approach in the AWS architecture blog
// article titled "Exponential Backoff And Jitter", which is found at
// http://www.awsarchitectureblog.com/2015/03/backoff.html.
//
// Essentially, the backoff has an interval `time.Duration`; the nth
// call to backoff will return a `time.Duration` that is 2^n *
// interval. If jitter is enabled (which is the default behaviour),
// the duration is a random value between 0 and 2^n * interval.  The
// backoff is configured with a maximum duration that will not be
// exceeded.
//
// The `New` function will attempt to use the system's cryptographic
// random number generator to seed a Go math/rand random number
// source. If this fails, the package will panic on startup.

import (
	"crypto/rand"
	"encoding/binary"
	"io"
	"math"
	mrand "math/rand"
	"sync"
	"time"
)

var prngMu sync.Mutex
var prng *mrand.Rand

// DefaultInterval is used when a Backoff is initialised with a
// zero-value Interval.
var DefaultInterval = 5 * time.Minute

// DefaultMaxDuration is maximum amount of time that the backoff will
// delay for.
var DefaultMaxDuration = 6 * time.Hour

// A Backoff contains the information needed to intelligently backoff
// and retry operations using an exponential backoff algorithm. It should
// be initialised with a call to `New`.
//
// Only use a Backoff from a single goroutine, it is not safe for concurrent
// access.
type Backoff struct {
	// maxDuration is the largest possible duration that can be
	// returned from a call to Duration.
	maxDuration time.Duration

	// interval controls the time step for backing off.
	interval time.Duration

	// noJitter controls whether to use the "Full Jitter"
	// improvement to attempt to smooth out spikes in a high
	// contention scenario. If noJitter is set to true, no
	// jitter will be introduced.
	noJitter bool

	// decay controls the decay of n. If it is non-zero, n is
	// reset if more than the last backoff + decay has elapsed since
	// the last try.
	decay time.Duration

	n       uint64
	lastTry time.Time
}

// New creates a new backoff with the specified max duration and
// interval. Zero values may be used to use the default values.
//
// Panics if either max or interval is negative.
func New(max time.Duration, interval time.Duration) *Backoff {
	if max < 0 || interval < 0 {
		panic("backoff: max or interval is negative")
	}

	b := &Backoff{
		maxDuration: max,
		interval:    interval,
	}
	b.setup()
	return b
}

// NewWithoutJitter works similarly to New, except that the created
// Backoff will not use jitter.
func NewWithoutJitter(max time.Duration, interval time.Duration) *Backoff {
	b := New(max, interval)
	b.noJitter = true
	return b
}

func init() {
	var buf [8]byte
	var n int64

	_, err := io.ReadFull(rand.Reader, buf[:])
	if err != nil {
		panic(err.Error())
	}

	n = int64(binary.LittleEndian.Uint64(buf[:]))

	src := mrand.NewSource(n)
	prng = mrand.New(src)
}

func (b *Backoff) setup() {
	if b.interval == 0 {
		b.interval = DefaultInterval
	}

	if b.maxDuration == 0 {
		b.maxDuration = DefaultMaxDuration
	}
}

// Duration returns a time.Duration appropriate for the backoff,
// incrementing the attempt counter.
func (b *Backoff) Duration() time.Duration {
	b.setup()

	b.decayN()

	t := b.duration(b.n)

	if b.n < math.MaxUint64 {
		b.n++
	}

	if !b.noJitter {
		prngMu.Lock()
		t = time.Duration(prng.Int63n(int64(t)))
		prngMu.Unlock()
	}

	return t
}

// requires b to be locked.
func (b *Backoff) duration(n uint64) (t time.Duration) {
	// Saturate pow
	pow := time.Duration(math.MaxInt64)
	if n < 63 {
		pow = 1 << n
	}

	t = b.interval * pow
	if t/pow != b.interval || t > b.maxDuration {
		t = b.maxDuration
	}

	return
}

// Reset resets the attempt counter of a backoff.
//
// It should be called when the rate-limited action succeeds.
func (b *Backoff) Reset() {
	b.lastTry = time.Time{}
	b.n = 0
}

// SetDecay sets the duration after which the try counter will be reset.
// Panics if decay is smaller than 0.
//
// The decay only kicks in if at least the last backoff + decay has elapsed
// since the last try.
func (b *Backoff) SetDecay(decay time.Duration) {
	if decay < 0 {
		panic("backoff: decay < 0")
	}

	b.decay = decay
}

// requires b to be locked
func (b *Backoff) decayN() {
	if b.decay == 0 {
		return
	}

	if b.lastTry.IsZero() {
		b.lastTry = time.Now()
		return
	}

	lastDuration := b.duration(b.n - 1)
	decayed := time.Since(b.lastTry) > lastDuration+b.decay
	b.lastTry = time.Now()

	if !decayed {
		return
	}

	b.n = 0
}