Files
mosaic/utils/api_limiter.py
T

93 lines
3.1 KiB
Python

"""Exponential backoff rate limiter with circuit breaker for Anthropic API."""
import time
import random
import threading
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class CircuitBreakerOpen(Exception):
"""Raised when circuit breaker is open (too many consecutive failures)."""
pass
class APIRateLimiter:
"""Exponential backoff with jitter and circuit breaker for API calls."""
def __init__(self, base_delay: float = 1.0, max_delay: float = 60.0,
circuit_threshold: int = 5):
"""
Args:
base_delay: Initial backoff delay in seconds
max_delay: Maximum backoff delay in seconds
circuit_threshold: Consecutive failures before circuit opens
"""
self.base_delay = base_delay
self.max_delay = max_delay
self.circuit_threshold = circuit_threshold
self._consecutive_failures = 0
self._current_delay = 0.0
self._lock = threading.Lock()
@property
def is_circuit_open(self) -> bool:
return self._consecutive_failures >= self.circuit_threshold
def record_success(self):
"""Record a successful API call — reset backoff."""
with self._lock:
self._consecutive_failures = 0
self._current_delay = 0.0
def record_failure(self, error: Optional[str] = None):
"""Record a failed API call — increase backoff."""
with self._lock:
self._consecutive_failures += 1
logger.warning(
"API failure #%d/%d: %s",
self._consecutive_failures,
self.circuit_threshold,
error or "unknown"
)
if self.is_circuit_open:
logger.error(
"Circuit breaker OPEN: %d consecutive failures. "
"Halting API calls until manual reset.",
self._consecutive_failures
)
def wait_if_needed(self):
"""Wait with exponential backoff if there have been failures. Raises CircuitBreakerOpen."""
with self._lock:
if self.is_circuit_open:
raise CircuitBreakerOpen(
f"Circuit breaker open after {self._consecutive_failures} consecutive failures. "
f"Call reset() to retry."
)
if self._consecutive_failures == 0:
return
delay = min(
self.base_delay * (2 ** (self._consecutive_failures - 1)),
self.max_delay
)
# Add jitter (±25%)
jitter = delay * random.uniform(-0.25, 0.25)
actual_delay = max(0, delay + jitter)
logger.info("API backoff: waiting %.1fs (attempt %d)",
actual_delay, self._consecutive_failures + 1)
# Sleep outside the lock
time.sleep(actual_delay)
def reset(self):
"""Manually reset the circuit breaker."""
with self._lock:
self._consecutive_failures = 0
self._current_delay = 0.0
logger.info("Circuit breaker reset")