Retry calls fn repeatedly until it succeeds, returns a non-retryable error, ctx is canceled, or MaxAttempts is reached. Retries use exponential backoff capped at MaxDelay, unless the normalized error includes a longer provider Retry-After hint. The onRetry callback (if non-nil) is called before eac
(ctx context.Context, fn RetryFn, onRetry OnRetryFn)
| 79 | // attempt, giving the caller a chance to reset state, log, or |
| 80 | // publish status events. |
| 81 | func Retry(ctx context.Context, fn RetryFn, onRetry OnRetryFn) error { |
| 82 | var attempt int |
| 83 | for { |
| 84 | err := fn(ctx) |
| 85 | if err == nil { |
| 86 | return nil |
| 87 | } |
| 88 | |
| 89 | classified := chaterror.Classify(err) |
| 90 | if !classified.Retryable { |
| 91 | return chaterror.WithClassification(err, classified) |
| 92 | } |
| 93 | |
| 94 | // If the caller's context is already done, return the |
| 95 | // context error so cancellation propagates cleanly. |
| 96 | if ctx.Err() != nil { |
| 97 | return ctx.Err() |
| 98 | } |
| 99 | |
| 100 | attempt++ |
| 101 | if attempt >= MaxAttempts { |
| 102 | return chaterror.WithClassification( |
| 103 | xerrors.Errorf("max retry attempts (%d) exceeded: %w", MaxAttempts, err), |
| 104 | classified, |
| 105 | ) |
| 106 | } |
| 107 | |
| 108 | delay := effectiveDelay(attempt-1, classified) |
| 109 | |
| 110 | if onRetry != nil { |
| 111 | onRetry(attempt, err, classified, delay) |
| 112 | } |
| 113 | |
| 114 | timer := time.NewTimer(delay) |
| 115 | select { |
| 116 | case <-ctx.Done(): |
| 117 | timer.Stop() |
| 118 | return ctx.Err() |
| 119 | case <-timer.C: |
| 120 | } |
| 121 | } |
| 122 | } |