93 lines
2.5 KiB
Python
93 lines
2.5 KiB
Python
"""
|
|
Retry utilities with exponential backoff.
|
|
"""
|
|
|
|
import asyncio
|
|
from functools import wraps
|
|
from typing import Callable, TypeVar, Any, Tuple, Type
|
|
import logging
|
|
|
|
from tenacity import (
|
|
retry,
|
|
stop_after_attempt,
|
|
wait_exponential,
|
|
retry_if_exception_type,
|
|
before_sleep_log,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def retry_with_backoff(
|
|
max_attempts: int = 3,
|
|
min_wait: float = 1.0,
|
|
max_wait: float = 60.0,
|
|
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
) -> Callable:
|
|
"""
|
|
Decorator for retrying functions with exponential backoff.
|
|
|
|
Args:
|
|
max_attempts: Maximum number of retry attempts
|
|
min_wait: Minimum wait time between retries (seconds)
|
|
max_wait: Maximum wait time between retries (seconds)
|
|
exceptions: Tuple of exception types to retry on
|
|
|
|
Returns:
|
|
Decorated function with retry logic
|
|
"""
|
|
return retry(
|
|
stop=stop_after_attempt(max_attempts),
|
|
wait=wait_exponential(multiplier=1, min=min_wait, max=max_wait),
|
|
retry=retry_if_exception_type(exceptions),
|
|
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
reraise=True,
|
|
)
|
|
|
|
|
|
async def async_retry_with_backoff(
|
|
func: Callable[..., T],
|
|
*args,
|
|
max_attempts: int = 3,
|
|
min_wait: float = 1.0,
|
|
max_wait: float = 60.0,
|
|
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
**kwargs,
|
|
) -> T:
|
|
"""
|
|
Async function with retry logic and exponential backoff.
|
|
|
|
Args:
|
|
func: Async function to call
|
|
*args: Positional arguments for func
|
|
max_attempts: Maximum number of retry attempts
|
|
min_wait: Minimum wait time between retries (seconds)
|
|
max_wait: Maximum wait time between retries (seconds)
|
|
exceptions: Tuple of exception types to retry on
|
|
**kwargs: Keyword arguments for func
|
|
|
|
Returns:
|
|
Result of the function call
|
|
"""
|
|
last_exception = None
|
|
wait_time = min_wait
|
|
|
|
for attempt in range(1, max_attempts + 1):
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except exceptions as e:
|
|
last_exception = e
|
|
if attempt < max_attempts:
|
|
logger.warning(
|
|
f"Attempt {attempt}/{max_attempts} failed: {e}. "
|
|
f"Retrying in {wait_time:.1f}s..."
|
|
)
|
|
await asyncio.sleep(wait_time)
|
|
wait_time = min(wait_time * 2, max_wait)
|
|
else:
|
|
logger.error(f"All {max_attempts} attempts failed: {e}")
|
|
|
|
raise last_exception
|