-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathretry.go
More file actions
148 lines (127 loc) · 3.54 KB
/
Copy pathretry.go
File metadata and controls
148 lines (127 loc) · 3.54 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
package httputil
import (
"fmt"
"io"
"math"
"net/http"
"time"
)
// RetryTransport wraps an http.RoundTripper to add headers, retries, and exponential backoff
type retryTransport struct {
Transport http.RoundTripper
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
Headers map[string]string
}
type retryTransportOpt func(*retryTransport) error
func WithMaxRetries(maxRetries int) retryTransportOpt {
return func(t *retryTransport) error {
if maxRetries < 0 {
return fmt.Errorf("maxRetries must be non-negative")
}
t.MaxRetries = maxRetries
return nil
}
}
func WithInitialDelay(delay time.Duration) retryTransportOpt {
return func(t *retryTransport) error {
if delay <= 0 {
return fmt.Errorf("initial delay must be positive")
}
t.InitialDelay = delay
return nil
}
}
func WithMaxDelay(delay time.Duration) retryTransportOpt {
return func(t *retryTransport) error {
if delay <= 0 {
return fmt.Errorf("max delay must be positive")
}
t.MaxDelay = delay
return nil
}
}
func WithHeaders(headers map[string]string) retryTransportOpt {
return func(t *retryTransport) error {
if headers == nil {
return fmt.Errorf("headers cannot be nil")
}
t.Headers = headers
return nil
}
}
// RoundTrip implements the http.RoundTripper interface
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Inject headers into every request
for k, v := range t.Headers {
req.Header.Set(k, v)
}
var resp *http.Response
var err error
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
// Clone the request body if it exists (for retries)
var bodyClone io.ReadCloser
if req.Body != nil && req.GetBody != nil {
bodyClone, err = req.GetBody()
if err != nil {
return nil, fmt.Errorf("failed to clone request body: %w", err)
}
req.Body = bodyClone
}
// Make the actual request
resp, err = t.Transport.RoundTrip(req)
// If successful or non-retryable, return
if err == nil && !shouldRetry(resp.StatusCode) {
return resp, nil
}
// Close response body if it exists
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
// Don't sleep after the last attempt
if attempt < t.MaxRetries {
delay := t.calculateBackoff(attempt)
fmt.Printf("Attempt %d failed, retrying in %v...\n", attempt+1, delay)
// Use context-aware sleep to respect cancellation
select {
case <-req.Context().Done():
return nil, req.Context().Err()
case <-time.After(delay):
// Continue to next retry
}
}
}
return resp, fmt.Errorf("max retries exceeded: %w", err)
}
// calculateBackoff calculates exponential backoff with max delay
func (t *retryTransport) calculateBackoff(attempt int) time.Duration {
delay := time.Duration(float64(t.InitialDelay) * math.Pow(2, float64(attempt)))
if delay > t.MaxDelay {
delay = t.MaxDelay
}
return delay
}
// shouldRetry determines if a status code should trigger a retry
func shouldRetry(statusCode int) bool {
// Retry on 5xx server errors and 429 Too Many Requests
return statusCode >= 500 || statusCode == 429
}
// NewRetryClient creates an HTTP client with retry logic and header injection
func NewRetryClient(opts ...retryTransportOpt) (*http.Client, error) {
transport := &retryTransport{
Transport: http.DefaultTransport,
MaxRetries: 3,
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Headers: make(map[string]string),
}
for _, opt := range opts {
if err := opt(transport); err != nil {
return nil, err
}
}
return &http.Client{
Transport: transport,
}, nil
}