-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgoop-retry.go
More file actions
229 lines (184 loc) · 6.68 KB
/
Copy pathgoop-retry.go
File metadata and controls
229 lines (184 loc) · 6.68 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
package goop
import (
"fmt"
"math"
"time"
)
// RetryConfig defines retry behavior
type RetryConfig struct {
MaxAttempts int // Maximum number of retry attempts
BaseDelay time.Duration // Base delay between retries
MaxDelay time.Duration // Maximum delay between retries
Multiplier float64 // Backoff multiplier
Retryable func(error) bool // Function to determine if error is retryable
}
// DefaultRetryConfig provides sensible defaults
var DefaultRetryConfig = RetryConfig{
MaxAttempts: 3,
BaseDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
Retryable: isRetryableError,
}
// Global retry configuration
var globalRetryConfig = DefaultRetryConfig
// SetRetryConfig sets the global retry configuration
func SetRetryConfig(config RetryConfig) {
globalRetryConfig = config
}
// GetRetryConfig returns the current global retry configuration
func GetRetryConfig() RetryConfig {
return globalRetryConfig
}
// NewRetryConfig creates a new retry configuration
func NewRetryConfig(maxAttempts int, baseDelay, maxDelay time.Duration) RetryConfig {
return RetryConfig{
MaxAttempts: maxAttempts,
BaseDelay: baseDelay,
MaxDelay: maxDelay,
Multiplier: 2.0,
Retryable: isRetryableError,
}
}
// isRetryableError determines if an error should be retried
func isRetryableError(err error) bool {
if err == nil {
return false
}
// Check if it's a Goop error
if goopErr, ok := err.(Error); ok {
switch goopErr.Type {
case ErrInGetRequest, ErrTimeout:
return true
case ErrCreatingGetRequest, ErrUnableToParse, ErrElementNotFound:
return false
default:
return true // Retry other errors by default
}
}
// For non-Goop errors, assume they're retryable (network errors, etc.)
return true
}
// calculateDelay calculates the delay for a given attempt using exponential backoff
func calculateDelay(attempt int, config RetryConfig) time.Duration {
if attempt <= 0 {
return 0
}
// Exponential backoff: delay = baseDelay * (multiplier ^ (attempt - 1))
delay := float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt-1))
// Apply jitter (±25% random variation)
jitter := delay * 0.25 * (2*float64(time.Now().UnixNano()%1000)/1000 - 1)
delay += jitter
// Ensure delay doesn't exceed max delay
if delay > float64(config.MaxDelay) {
delay = float64(config.MaxDelay)
}
return time.Duration(delay)
}
// RetryGet performs a GET request with retry logic
func RetryGet(url string, timeout time.Duration) (string, error) {
return RetryGetWithConfig(url, timeout, globalRetryConfig)
}
// RetryGetWithConfig performs a GET request with custom retry configuration
func RetryGetWithConfig(url string, timeout time.Duration, config RetryConfig) (string, error) {
var lastErr error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
debugLog(DebugVerbose, "HTTP GET attempt %d/%d for %s", attempt, config.MaxAttempts, url)
// Apply rate limiting
waitForRateLimit()
resp, err := GetWithTimeout(url, timeout)
if err == nil {
debugLog(DebugVerbose, "HTTP GET succeeded on attempt %d for %s", attempt, url)
return resp, nil
}
lastErr = err
// Check if error is retryable
if !config.Retryable(err) {
debugLog(DebugBasic, "HTTP GET failed with non-retryable error: %v", err)
return "", err
}
// If this is the last attempt, don't wait
if attempt == config.MaxAttempts {
break
}
// Calculate delay and wait
delay := calculateDelay(attempt, config)
debugLog(DebugVerbose, "HTTP GET failed (attempt %d), retrying in %v: %v", attempt, delay, err)
time.Sleep(delay)
}
debugLog(DebugBasic, "HTTP GET failed after %d attempts: %v", config.MaxAttempts, lastErr)
return "", fmt.Errorf("failed after %d attempts: %w", config.MaxAttempts, lastErr)
}
// RetryPost performs a POST request with retry logic
func RetryPost(url string, bodyType string, body interface{}, timeout time.Duration) (string, error) {
return RetryPostWithConfig(url, bodyType, body, timeout, globalRetryConfig)
}
// RetryPostWithConfig performs a POST request with custom retry configuration
func RetryPostWithConfig(url string, bodyType string, body interface{}, timeout time.Duration, config RetryConfig) (string, error) {
var lastErr error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
debugLog(DebugVerbose, "HTTP POST attempt %d/%d for %s", attempt, config.MaxAttempts, url)
// Apply rate limiting
waitForRateLimit()
resp, err := PostWithTimeout(url, bodyType, body, timeout)
if err == nil {
debugLog(DebugVerbose, "HTTP POST succeeded on attempt %d for %s", attempt, url)
return resp, nil
}
lastErr = err
// Check if error is retryable
if !config.Retryable(err) {
debugLog(DebugBasic, "HTTP POST failed with non-retryable error: %v", err)
return "", err
}
// If this is the last attempt, don't wait
if attempt == config.MaxAttempts {
break
}
// Calculate delay and wait
delay := calculateDelay(attempt, config)
debugLog(DebugVerbose, "HTTP POST failed (attempt %d), retrying in %v: %v", attempt, delay, err)
time.Sleep(delay)
}
debugLog(DebugBasic, "HTTP POST failed after %d attempts: %v", config.MaxAttempts, lastErr)
return "", fmt.Errorf("failed after %d attempts: %w", config.MaxAttempts, lastErr)
}
// RetryOperation executes any function with retry logic
func RetryOperation(operation func() error, config RetryConfig) error {
var lastErr error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
debugLog(DebugVerbose, "Operation attempt %d/%d", attempt, config.MaxAttempts)
err := operation()
if err == nil {
debugLog(DebugVerbose, "Operation succeeded on attempt %d", attempt)
return nil
}
lastErr = err
// Check if error is retryable
if !config.Retryable(err) {
debugLog(DebugBasic, "Operation failed with non-retryable error: %v", err)
return err
}
// If this is the last attempt, don't wait
if attempt == config.MaxAttempts {
break
}
// Calculate delay and wait
delay := calculateDelay(attempt, config)
debugLog(DebugVerbose, "Operation failed (attempt %d), retrying in %v: %v", attempt, delay, err)
time.Sleep(delay)
}
debugLog(DebugBasic, "Operation failed after %d attempts: %v", config.MaxAttempts, lastErr)
return fmt.Errorf("failed after %d attempts: %w", config.MaxAttempts, lastErr)
}
// RetryWithBackoff is a convenience function for simple retry scenarios
func RetryWithBackoff(maxAttempts int, baseDelay time.Duration, operation func() error) error {
config := RetryConfig{
MaxAttempts: maxAttempts,
BaseDelay: baseDelay,
MaxDelay: baseDelay * time.Duration(math.Pow(2, float64(maxAttempts-1))),
Multiplier: 2.0,
Retryable: isRetryableError,
}
return RetryOperation(operation, config)
}