( ctx context.Context, req *common.NormalizedRequest, hedged func(ctx context.Context) (*common.NormalizedResponse, error), )
| 203 | } |
| 204 | |
| 205 | func (e *networkExecutor) runRetry( |
| 206 | ctx context.Context, |
| 207 | req *common.NormalizedRequest, |
| 208 | hedged func(ctx context.Context) (*common.NormalizedResponse, error), |
| 209 | ) (*common.NormalizedResponse, error) { |
| 210 | maxAttempts := 1 |
| 211 | if e.cfg != nil && e.cfg.Retry != nil && e.cfg.Retry.MaxAttempts > 0 { |
| 212 | maxAttempts = e.cfg.Retry.MaxAttempts |
| 213 | } |
| 214 | startTime := time.Now() |
| 215 | st := req.ExecState() |
| 216 | |
| 217 | var bestResp *common.NormalizedResponse |
| 218 | var lastErr error |
| 219 | // firstInformativeErr captures the first attempt's error that |
| 220 | // contains real upstream details (ErrUpstreamsExhausted with |
| 221 | // children, ErrExecutionException, etc.) — subsequent retry |
| 222 | // attempts can degenerate into bare ErrNoUpstreamsLeftToSelect |
| 223 | // which loses that info; the final wrap uses this instead. |
| 224 | var firstInformativeErr error |
| 225 | retriesAttempted := 0 |
| 226 | |
| 227 | for attempt := 0; attempt < maxAttempts; attempt++ { |
| 228 | // Bail out early if the lifecycle context has fired — the timeout |
| 229 | // owns the classification, not retry-exhausted. |
| 230 | if ctxErr := ctx.Err(); ctxErr != nil { |
| 231 | cause := context.Cause(ctx) |
| 232 | if cause != nil { |
| 233 | return bestResp, cause |
| 234 | } |
| 235 | return bestResp, ctxErr |
| 236 | } |
| 237 | |
| 238 | if attempt > 0 && st != nil { |
| 239 | st.NetworkRetries.Add(1) |
| 240 | retriesAttempted++ |
| 241 | } |
| 242 | if st != nil { |
| 243 | st.NetworkAttempts.Add(1) |
| 244 | } |
| 245 | |
| 246 | resp, err := hedged(ctx) |
| 247 | |
| 248 | // If the lifecycle context fired during the attempt, surface the |
| 249 | // ctx cause directly instead of wrapping as retry-exhausted. |
| 250 | if ctxErr := ctx.Err(); ctxErr != nil { |
| 251 | cause := context.Cause(ctx) |
| 252 | if cause != nil { |
| 253 | return bestResp, cause |
| 254 | } |
| 255 | return bestResp, ctxErr |
| 256 | } |
| 257 | |
| 258 | // If this is the last attempt OR shouldRetry says no, return. |
| 259 | retryReason := "" |
| 260 | if attempt+1 < maxAttempts { |
| 261 | retryReason = e.shouldRetryWithReason(req, resp, err, attempt) |
| 262 | } |
no test coverage detected