diff options
Diffstat (limited to 'internal/executor/ratelimit.go')
| -rw-r--r-- | internal/executor/ratelimit.go | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/internal/executor/ratelimit.go b/internal/executor/ratelimit.go new file mode 100644 index 0000000..884da43 --- /dev/null +++ b/internal/executor/ratelimit.go @@ -0,0 +1,76 @@ +package executor + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var retryAfterRe = regexp.MustCompile(`(?i)retry[-_ ]after[:\s]+(\d+)`) + +const maxBackoffDelay = 5 * time.Minute + +// isRateLimitError returns true if err looks like a Claude API rate-limit response. +func isRateLimitError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "rate limit") || + strings.Contains(msg, "too many requests") || + strings.Contains(msg, "429") || + strings.Contains(msg, "overloaded") +} + +// parseRetryAfter extracts a Retry-After duration from an error message. +// Returns 0 if no retry-after value is found. +func parseRetryAfter(msg string) time.Duration { + m := retryAfterRe.FindStringSubmatch(msg) + if m == nil { + return 0 + } + secs, err := strconv.Atoi(m[1]) + if err != nil || secs <= 0 { + return 0 + } + return time.Duration(secs) * time.Second +} + +// runWithBackoff calls fn repeatedly on rate-limit errors, using exponential backoff. +// maxRetries is the max number of retry attempts (not counting the initial call). +// baseDelay is the initial backoff duration (doubled each retry). +func runWithBackoff(ctx context.Context, maxRetries int, baseDelay time.Duration, fn func() error) error { + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + lastErr = fn() + if lastErr == nil { + return nil + } + if !isRateLimitError(lastErr) { + return lastErr + } + if attempt == maxRetries { + break + } + + // Compute exponential backoff delay. + delay := baseDelay * (1 << attempt) + if delay > maxBackoffDelay { + delay = maxBackoffDelay + } + // Use Retry-After header value if present. + if ra := parseRetryAfter(lastErr.Error()); ra > 0 { + delay = ra + } + + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled during rate-limit backoff: %w", ctx.Err()) + case <-time.After(delay): + } + } + return lastErr +} |
