-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpolicies.go
More file actions
290 lines (254 loc) · 9.09 KB
/
Copy pathpolicies.go
File metadata and controls
290 lines (254 loc) · 9.09 KB
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// Package goretry provides retry mechanisms with various backoff policies and transient error strategies.
// This file contains implementations of different retry policies that determine the delay between retry attempts.
package goretry
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"time"
)
// FixedDelayPolicy implements a retry policy with a constant delay between attempts.
// This policy provides the simplest retry strategy where each retry waits the same amount of time.
// It's useful when you want predictable, consistent timing between retries.
//
// Example:
//
// policy := NewFixedDelayPolicy(500 * time.Millisecond)
// retrier := NewRetrier(policy)
type FixedDelayPolicy struct {
// Delay is the fixed duration to wait between retry attempts
Delay time.Duration
}
// NewFixedDelayPolicy creates a new FixedDelayPolicy with the specified delay duration.
// The delay will be applied between each retry attempt.
//
// Example:
//
// policy := NewFixedDelayPolicy(1 * time.Second) // 1 second between each retry
func NewFixedDelayPolicy(delay time.Duration) *FixedDelayPolicy {
if delay < 0 {
panic(fmt.Sprintf("delay must be non-negative, got %v", delay))
}
return &FixedDelayPolicy{Delay: delay}
}
// NextDelay returns the fixed delay for any attempt number.
// It always returns the configured delay and true (indicating retries should continue).
func (p *FixedDelayPolicy) NextDelay(attempt int) (time.Duration, bool) {
return p.Delay, true
}
// ExponentialBackoffPolicy implements exponential backoff with optional jitter.
// This policy increases the delay exponentially with each retry attempt, which helps
// reduce load on failing services and increases the chance of recovery.
// Jitter can be enabled to add randomness and prevent thundering herd problems.
//
// The delay is calculated as: BaseDelay * (Multiplier ^ (attempt-1))
// If jitter is enabled, the actual delay will be randomized between delay/2 and delay.
//
// Example:
//
// policy := NewExponentialBackoffPolicy(100*time.Millisecond, 30*time.Second).
// WithMultiplier(2.0).
// WithJitter(true)
type ExponentialBackoffPolicy struct {
// BaseDelay is the initial delay for the first retry
BaseDelay time.Duration
// MaxDelay is the maximum delay that will be applied, providing an upper bound
MaxDelay time.Duration
// Multiplier is the factor by which the delay increases each attempt (default: 2.0)
Multiplier float64
// Jitter adds randomness to delays to prevent thundering herd (default: true)
Jitter bool
}
// NewExponentialBackoffPolicy creates a new ExponentialBackoffPolicy with sensible defaults.
// The default multiplier is 2.0 and jitter is enabled by default.
//
// Parameters:
// - baseDelay: the initial delay for the first retry attempt
// - maxDelay: the maximum delay that will be applied (prevents infinite growth)
//
// Example:
//
// policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second)
func NewExponentialBackoffPolicy(baseDelay, maxDelay time.Duration) *ExponentialBackoffPolicy {
if baseDelay < 0 {
panic(fmt.Sprintf("baseDelay must be non-negative, got %v", baseDelay))
}
if maxDelay < 0 {
panic(fmt.Sprintf("maxDelay must be non-negative, got %v", maxDelay))
}
if maxDelay < baseDelay {
panic(fmt.Sprintf("maxDelay (%v) must be >= baseDelay (%v)", maxDelay, baseDelay))
}
return &ExponentialBackoffPolicy{
BaseDelay: baseDelay,
MaxDelay: maxDelay,
Multiplier: 2.0,
Jitter: true,
}
}
// WithMultiplier sets the multiplier used for exponential growth.
// The default multiplier is 2.0. Common values are between 1.5 and 3.0.
// Returns the policy instance for method chaining.
//
// Example:
//
// policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second).
// WithMultiplier(1.5) // Slower growth than default
func (p *ExponentialBackoffPolicy) WithMultiplier(multiplier float64) *ExponentialBackoffPolicy {
if multiplier <= 1.0 {
panic(fmt.Sprintf("multiplier must be > 1.0, got %v", multiplier))
}
if math.IsInf(multiplier, 0) || math.IsNaN(multiplier) {
panic(fmt.Sprintf("multiplier must be a finite number, got %v", multiplier))
}
p.Multiplier = multiplier
return p
}
// WithJitter controls whether jitter (randomness) is applied to delays.
// Jitter is enabled by default and helps prevent thundering herd problems
// when multiple clients retry simultaneously.
// Returns the policy instance for method chaining.
//
// Example:
//
// policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second).
// WithJitter(false) // Disable jitter for predictable delays
func (p *ExponentialBackoffPolicy) WithJitter(jitter bool) *ExponentialBackoffPolicy {
p.Jitter = jitter
return p
}
// NextDelay calculates the delay for the next retry attempt using exponential backoff.
// The delay grows exponentially with each attempt, capped at MaxDelay.
// If jitter is enabled, the delay is randomized between delay/2 and delay using cryptographically secure randomness.
func (p *ExponentialBackoffPolicy) NextDelay(attempt int) (time.Duration, bool) {
if attempt <= 0 {
return 0, false
}
// Calculate exponential backoff with overflow protection
exponent := float64(attempt - 1)
multiplierPower := math.Pow(p.Multiplier, exponent)
// Check for overflow or infinity
if math.IsInf(multiplierPower, 0) || multiplierPower > float64(p.MaxDelay)/float64(p.BaseDelay) {
delay := p.MaxDelay
return p.applyJitter(delay), true
}
delay := time.Duration(float64(p.BaseDelay) * multiplierPower)
// Ensure we don't exceed MaxDelay
if delay > p.MaxDelay || delay < 0 { // negative check for overflow
delay = p.MaxDelay
}
return p.applyJitter(delay), true
}
// applyJitter applies cryptographically secure jitter to the delay if enabled.
// Jitter randomizes the delay between delay/2 and delay to prevent thundering herd.
func (p *ExponentialBackoffPolicy) applyJitter(delay time.Duration) time.Duration {
if !p.Jitter || delay <= 0 {
return delay
}
// Use crypto/rand for thread-safe, cryptographically secure randomness
halfDelay := delay / 2
maxJitter := int64(delay - halfDelay)
if maxJitter <= 0 {
return delay
}
// Generate secure random number
jitterBig, err := rand.Int(rand.Reader, big.NewInt(maxJitter))
if err != nil {
// Fallback to no jitter if crypto/rand fails
return delay
}
jitter := time.Duration(jitterBig.Int64())
return halfDelay + jitter
}
// LinearBackoffPolicy implements linear backoff
type LinearBackoffPolicy struct {
BaseDelay time.Duration
MaxDelay time.Duration
Increment time.Duration
}
func NewLinearBackoffPolicy(baseDelay, increment, maxDelay time.Duration) *LinearBackoffPolicy {
if baseDelay < 0 {
panic(fmt.Sprintf("baseDelay must be non-negative, got %v", baseDelay))
}
if increment < 0 {
panic(fmt.Sprintf("increment must be non-negative, got %v", increment))
}
if maxDelay < 0 {
panic(fmt.Sprintf("maxDelay must be non-negative, got %v", maxDelay))
}
if maxDelay < baseDelay {
panic(fmt.Sprintf("maxDelay (%v) must be >= baseDelay (%v)", maxDelay, baseDelay))
}
return &LinearBackoffPolicy{
BaseDelay: baseDelay,
MaxDelay: maxDelay,
Increment: increment,
}
}
func (p *LinearBackoffPolicy) NextDelay(attempt int) (time.Duration, bool) {
if attempt <= 0 {
return 0, false
}
// Protect against overflow in multiplication
increment := time.Duration(attempt-1) * p.Increment
if attempt > 1 && increment < 0 { // Overflow check
return p.MaxDelay, true
}
delay := p.BaseDelay + increment
// Check for overflow or exceeding max
if delay < p.BaseDelay || delay > p.MaxDelay {
delay = p.MaxDelay
}
return delay, true
}
// NoDelayPolicy implements immediate retry with no delay
type NoDelayPolicy struct{}
func NewNoDelayPolicy() *NoDelayPolicy {
return &NoDelayPolicy{}
}
func (p *NoDelayPolicy) NextDelay(attempt int) (time.Duration, bool) {
return 0, true
}
// StopPolicy wraps another policy and stops retrying after a specified condition.
// Note: For duration-based stopping, timing is managed by the retrier, not the policy,
// to ensure thread safety and correct timing behavior.
type StopPolicy struct {
policy RetryPolicy
maxAttempts int
maxDuration time.Duration
}
func NewStopPolicy(policy RetryPolicy) *StopPolicy {
if policy == nil {
panic("policy cannot be nil")
}
return &StopPolicy{
policy: policy,
}
}
func (p *StopPolicy) WithMaxAttempts(attempts int) *StopPolicy {
if attempts < 0 {
panic(fmt.Sprintf("maxAttempts must be non-negative, got %d", attempts))
}
p.maxAttempts = attempts
return p
}
func (p *StopPolicy) WithMaxDuration(duration time.Duration) *StopPolicy {
if duration < 0 {
panic(fmt.Sprintf("maxDuration must be non-negative, got %v", duration))
}
p.maxDuration = duration
return p
}
func (p *StopPolicy) NextDelay(attempt int) (time.Duration, bool) {
if p.maxAttempts > 0 && attempt >= p.maxAttempts {
return 0, false
}
// Duration checking will be handled by the retrier that maintains the start time
// This ensures thread safety and proper timing behavior per retry operation
return p.policy.NextDelay(attempt)
}
// GetMaxDuration returns the maximum duration setting for use by the retrier
func (p *StopPolicy) GetMaxDuration() time.Duration {
return p.maxDuration
}