41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
"""Small, bounded retry primitives shared by outbound provider requests."""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from collections.abc import Callable
|
|
|
|
import requests
|
|
|
|
_RETRYABLE_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504})
|
|
|
|
|
|
def retry_delay(attempt: int, *, base: float = 0.5, cap: float = 8.0) -> float:
|
|
"""Return bounded exponential backoff for the zero-based retry attempt."""
|
|
return min(float(cap), max(0.0, float(base)) * (2 ** max(0, int(attempt))))
|
|
|
|
|
|
def request_with_retries(method: str, url: str, *, attempts: int = 3,
|
|
sleep: Callable[[float], None] | None = None, **kwargs):
|
|
"""Make a request, retrying only transient transport/upstream failures.
|
|
|
|
The final response or exception is returned to the caller unchanged. Any
|
|
response discarded before a retry is closed so streaming sockets cannot
|
|
accumulate while a provider is cold or rate-limiting.
|
|
"""
|
|
attempts = max(1, min(int(attempts), 5))
|
|
sleep = sleep or time.sleep
|
|
last_response = None
|
|
for attempt in range(attempts):
|
|
try:
|
|
response = requests.request(method, url, **kwargs)
|
|
last_response = response
|
|
if response.status_code not in _RETRYABLE_STATUS_CODES or attempt >= attempts - 1:
|
|
return response
|
|
response.close()
|
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
|
if attempt >= attempts - 1:
|
|
raise
|
|
if attempt < attempts - 1:
|
|
sleep(retry_delay(attempt))
|
|
return last_response
|