UNPKG

voyageai-cli

Version:

CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search

267 lines (205 loc) 6.68 kB
# Retry Strategies Implementing robust retry logic is critical for resilient applications. This guide covers retry strategies for different failure scenarios. ## When to Retry **Retryable errors**: - Network timeouts (temporary connectivity issues) - 429 Too Many Requests (rate limited; will recover) - 503 Service Unavailable (temporary outage) - 500 Internal Server Error (may be transient) - Connection refused (service starting up) **Non-retryable errors**: - 401 Unauthorized (authentication failure; won't recover) - 403 Forbidden (authorization; retrying won't help) - 404 Not Found (resource doesn't exist) - 400 Bad Request (malformed request) - 422 Unprocessable Entity (invalid business logic) ## Exponential Backoff Wait longer between retries to give services time to recover: ```python max_retries = 5 base_wait = 0.1 # 100ms for attempt in range(max_retries): try: return api.call() except RetryableError: if attempt == max_retries - 1: raise wait = base_wait * (2 ** attempt) # Exponential growth time.sleep(wait) # Wait times: 100ms, 200ms, 400ms, 800ms, 1600ms ``` ## Jitter Add randomness to prevent thundering herd (all clients retrying simultaneously): ```python wait = base_wait * (2 ** attempt) jitter = random.uniform(0, wait * 0.1) time.sleep(wait + jitter) ``` Without jitter, if 1000 clients hit the same 429 error, all retry in unison, causing another spike. With jitter, retries are spread out. ## Capped Exponential Backoff Exponential backoff can grow unbounded. Cap the maximum: ```python max_wait = 60 # Cap at 60 seconds wait = min(base_wait * (2 ** attempt), max_wait) time.sleep(wait) # Wait times: 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 6.4s, 12.8s, 25.6s, 60s, 60s, 60s ``` ## Retry-After Header Respect the `Retry-After` header if provided: ```python if response.status == 429: retry_after = int(response.headers.get('Retry-After', 60)) time.sleep(retry_after) ``` The server tells you exactly how long to wait; trust it. ## Idempotent Retries Make retries safe by using idempotency keys: ```python import uuid idempotency_key = str(uuid.uuid4()) for attempt in range(max_retries): try: response = api.post( '/orders', data={...}, headers={'Idempotency-Key': idempotency_key} ) return response except RetryableError: time.sleep(backoff(attempt)) ``` Same `Idempotency-Key` + body = same response. Retrying is safe. ## Dead Letter Queue If retries exceed maximum, queue for later processing: ```python try: api.process_order(order_id) except Exception as e: if retry_count >= max_retries: # Queue for background job dead_letter_queue.put({ 'order_id': order_id, 'error': str(e), 'attempted_at': now(), 'retry_count': retry_count }) else: raise ``` Background jobs can process queued items later with exponential backoff. ## Circuit Breaker with Retries Combine retries with circuit breaker to avoid cascading failures: ```python breaker = CircuitBreaker(failure_threshold=5, timeout=60) try: breaker.call(api.get_user, user_id) except CircuitBreakerOpen: # Circuit open; don't retry return get_cached_user(user_id) except Exception as e: # Circuit closed or half-open; retry for attempt in range(max_retries): try: return breaker.call(api.get_user, user_id) except Exception: time.sleep(backoff(attempt)) ``` ## Deadline/Timeout Set absolute deadline for all retries: ```python import time deadline = time.time() + 30 # 30 second deadline while time.time() < deadline: try: return api.call() except RetryableError: remaining = deadline - time.time() if remaining <= 0: raise TimeoutError("Exceeded deadline") wait = min(backoff(attempt), remaining) time.sleep(wait) ``` Even with retries, don't wait forever. ## Retry Budgets Limit total retries to prevent excessive load: ```python # Each service gets 10% of requests as retries retry_budget = 0.1 total_requests = 0 retry_requests = 0 for request in incoming: try: api.call() except RetryableError: if retry_requests / max(1, total_requests) < retry_budget: # Budget available; retry retry_request() retry_requests += 1 else: # Budget exhausted; reject reject_request() total_requests += 1 ``` This prevents cascading failures when multiple services fail. ## Transient Failure Examples **Example 1: Connection Timeout** ```python for attempt in range(3): try: response = requests.get('https://api.example.com/users', timeout=5) return response.json() except requests.Timeout: if attempt == 2: raise time.sleep(1 * (2 ** attempt)) ``` **Example 2: Rate Limited (429)** ```python for attempt in range(5): try: response = api.list_users() return response except RateLimitError: retry_after = int(response.headers.get('Retry-After', 60)) time.sleep(retry_after) ``` **Example 3: Transient 500 Error** ```python for attempt in range(3): try: response = api.create_order(order_data) return response except ServerError as e: if e.status == 500 and attempt < 2: time.sleep(0.5 * (2 ** attempt)) else: raise ``` ## Testing Retries Test retry logic with mock failures: ```python class FailingAPI: def __init__(self, fail_times=1): self.call_count = 0 self.fail_times = fail_times def call(self): self.call_count += 1 if self.call_count <= self.fail_times: raise ConnectionError("Simulated failure") return {"success": True} # Test that 1 retry succeeds after 1 failure api = FailingAPI(fail_times=1) result = call_with_retries(api) assert result['success'] == True assert api.call_count == 2 # Called twice ``` ## Best Practices 1. **Always retry transient errors**: Network timeouts, rate limits, 5xx 2. **Never retry non-transient errors**: Auth failures, 404s, validation errors 3. **Use exponential backoff**: 100ms, 200ms, 400ms, ... 4. **Add jitter**: Prevent thundering herd 5. **Cap wait time**: Don't wait forever 6. **Use idempotency keys**: Make retries safe 7. **Set deadlines**: Absolute timeout for all retries 8. **Monitor retry rates**: Alert on abnormally high retry rates See [Error Handling](error-handling.md) for comprehensive error strategies.