@@ -53,96 +53,136 @@ func (h *RetryHandler) ExecuteWithRetry(
53
53
availableEndpoints := make ([]* domain.Endpoint , len (endpoints ))
54
54
copy (availableEndpoints , endpoints )
55
55
56
+ // Preserve request body for potential retries
57
+ bodyBytes , err := h .preserveRequestBody (r )
58
+ if err != nil {
59
+ return err
60
+ }
61
+
56
62
var lastErr error
57
63
maxRetries := len (endpoints )
58
64
attemptCount := 0
59
65
60
- // Preserve request body for potential retries
61
- var bodyBytes []byte
62
- if r .Body != nil && r .Body != http .NoBody {
63
- var err error
64
- bodyBytes , err = io .ReadAll (r .Body )
65
- if err != nil {
66
- h .logger .Error ("Failed to read request body for retry preservation" ,
67
- "error" , err )
68
- return fmt .Errorf ("failed to read request body: %w" , err )
69
- }
70
-
71
- // Close the original body and handle any error
72
- if err := r .Body .Close (); err != nil {
73
- h .logger .Warn ("Failed to close original request body" ,
74
- "error" , err )
75
- // Continue as the body has been read successfully
76
- }
77
-
78
- // Recreate the body for the first attempt
79
- r .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
80
- }
81
-
82
66
for attemptCount < maxRetries && len (availableEndpoints ) > 0 {
83
- // Check for context cancellation before each attempt
84
- select {
85
- case <- ctx .Done ():
86
- return fmt .Errorf ("request cancelled: %w" , ctx .Err ())
87
- default :
67
+ if err := h .checkContextCancellation (ctx ); err != nil {
68
+ return err
88
69
}
89
70
90
- // Reset body for retries (skip first iteration as body already set above)
91
- if bodyBytes != nil && attemptCount > 0 {
92
- r .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
93
- }
71
+ h .resetRequestBodyForRetry (r , bodyBytes , attemptCount )
94
72
95
73
endpoint , err := selector .Select (ctx , availableEndpoints )
96
74
if err != nil {
97
75
return fmt .Errorf ("endpoint selection failed: %w" , err )
98
76
}
99
77
100
78
attemptCount ++
101
- err = proxyFunc (ctx , w , r , endpoint , stats )
79
+ lastErr = h . executeProxyAttempt (ctx , w , r , endpoint , selector , stats , proxyFunc )
102
80
103
- if err == nil {
81
+ if lastErr == nil {
104
82
return nil
105
83
}
106
84
107
- lastErr = err
108
-
109
- if IsConnectionError (err ) {
110
- h .logger .Warn ("Connection failed to endpoint, marking as unhealthy" ,
111
- "endpoint" , endpoint .Name ,
112
- "error" , err ,
113
- "attempt" , attemptCount ,
114
- "remaining_endpoints" , len (availableEndpoints )- 1 )
115
-
116
- h .markEndpointUnhealthy (ctx , endpoint )
117
-
118
- // Remove failed endpoint in-place to avoid allocation
119
- // Find and remove the failed endpoint by shifting elements
120
- for i := 0 ; i < len (availableEndpoints ); i ++ {
121
- if availableEndpoints [i ].Name == endpoint .Name {
122
- // Remove element at index i by copying subsequent elements
123
- copy (availableEndpoints [i :], availableEndpoints [i + 1 :])
124
- availableEndpoints = availableEndpoints [:len (availableEndpoints )- 1 ]
125
- break
126
- }
127
- }
128
-
129
- if len (availableEndpoints ) > 0 && attemptCount < maxRetries {
130
- h .logger .Info ("Retrying request with different endpoint" ,
131
- "available_endpoints" , len (availableEndpoints ),
132
- "attempts_remaining" , maxRetries - attemptCount )
133
- continue
134
- }
135
- } else {
85
+ if ! IsConnectionError (lastErr ) {
136
86
// Non-connection error warrants immediate failure
137
- return err
87
+ return lastErr
88
+ }
89
+
90
+ // Handle connection error and retry logic
91
+ availableEndpoints = h .handleConnectionFailure (ctx , endpoint , lastErr , attemptCount , availableEndpoints , maxRetries )
92
+ }
93
+
94
+ return h .buildFinalError (availableEndpoints , maxRetries , lastErr )
95
+ }
96
+
97
+ // preserveRequestBody reads and preserves request body for potential retries
98
+ func (h * RetryHandler ) preserveRequestBody (r * http.Request ) ([]byte , error ) {
99
+ if r .Body == nil || r .Body == http .NoBody {
100
+ return nil , nil
101
+ }
102
+
103
+ bodyBytes , err := io .ReadAll (r .Body )
104
+ if err != nil {
105
+ h .logger .Error ("Failed to read request body for retry preservation" , "error" , err )
106
+ return nil , fmt .Errorf ("failed to read request body: %w" , err )
107
+ }
108
+
109
+ if err := r .Body .Close (); err != nil {
110
+ h .logger .Warn ("Failed to close original request body" , "error" , err )
111
+ }
112
+
113
+ // Recreate the body for the first attempt
114
+ r .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
115
+ return bodyBytes , nil
116
+ }
117
+
118
+ // checkContextCancellation verifies if the context has been cancelled
119
+ func (h * RetryHandler ) checkContextCancellation (ctx context.Context ) error {
120
+ select {
121
+ case <- ctx .Done ():
122
+ return fmt .Errorf ("request cancelled: %w" , ctx .Err ())
123
+ default :
124
+ return nil
125
+ }
126
+ }
127
+
128
+ // resetRequestBodyForRetry recreates request body for retry attempts
129
+ func (h * RetryHandler ) resetRequestBodyForRetry (r * http.Request , bodyBytes []byte , attemptCount int ) {
130
+ if bodyBytes != nil && attemptCount > 0 {
131
+ r .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
132
+ }
133
+ }
134
+
135
+ // executeProxyAttempt executes a single proxy attempt with connection counting
136
+ func (h * RetryHandler ) executeProxyAttempt (ctx context.Context , w http.ResponseWriter , r * http.Request ,
137
+ endpoint * domain.Endpoint , selector domain.EndpointSelector , stats * ports.RequestStats , proxyFunc ProxyFunc ) error {
138
+
139
+ selector .IncrementConnections (endpoint )
140
+ defer selector .DecrementConnections (endpoint )
141
+
142
+ return proxyFunc (ctx , w , r , endpoint , stats )
143
+ }
144
+
145
+ // handleConnectionFailure processes connection failures and manages endpoint removal
146
+ func (h * RetryHandler ) handleConnectionFailure (ctx context.Context , endpoint * domain.Endpoint ,
147
+ err error , attemptCount int , availableEndpoints []* domain.Endpoint , maxRetries int ) []* domain.Endpoint {
148
+
149
+ h .logger .Warn ("Connection failed to endpoint, marking as unhealthy" ,
150
+ "endpoint" , endpoint .Name ,
151
+ "error" , err ,
152
+ "attempt" , attemptCount ,
153
+ "remaining_endpoints" , len (availableEndpoints )- 1 )
154
+
155
+ h .markEndpointUnhealthy (ctx , endpoint )
156
+
157
+ // Remove failed endpoint from available list
158
+ updatedEndpoints := h .removeFailedEndpoint (availableEndpoints , endpoint )
159
+
160
+ if len (updatedEndpoints ) > 0 && attemptCount < maxRetries {
161
+ h .logger .Info ("Retrying request with different endpoint" ,
162
+ "available_endpoints" , len (updatedEndpoints ),
163
+ "attempts_remaining" , maxRetries - attemptCount )
164
+ }
165
+
166
+ return updatedEndpoints
167
+ }
168
+
169
+ // removeFailedEndpoint removes the failed endpoint from the available list
170
+ func (h * RetryHandler ) removeFailedEndpoint (endpoints []* domain.Endpoint , failedEndpoint * domain.Endpoint ) []* domain.Endpoint {
171
+ for i := 0 ; i < len (endpoints ); i ++ {
172
+ if endpoints [i ].Name == failedEndpoint .Name {
173
+ // Remove element at index i by copying subsequent elements
174
+ copy (endpoints [i :], endpoints [i + 1 :])
175
+ return endpoints [:len (endpoints )- 1 ]
138
176
}
139
177
}
178
+ return endpoints
179
+ }
140
180
141
- // All endpoints exhausted or max attempts reached
181
+ // buildFinalError constructs the appropriate error message for retry failure
182
+ func (h * RetryHandler ) buildFinalError (availableEndpoints []* domain.Endpoint , maxRetries int , lastErr error ) error {
142
183
if len (availableEndpoints ) == 0 {
143
184
return fmt .Errorf ("all endpoints failed with connection errors: %w" , lastErr )
144
185
}
145
-
146
186
return fmt .Errorf ("max attempts (%d) reached: %w" , maxRetries , lastErr )
147
187
}
148
188
0 commit comments