AbdulElahGwaith's picture
Upload folder using huggingface_hub
dc893fb verified
"""Elegant retry mechanism module
Provides decorators and utility functions to support retry logic for async functions.
Features:
- Supports exponential backoff strategy
- Configurable retry count and intervals
- Supports specifying retryable exception types
- Detailed logging
- Fully decoupled, non-invasive to business code
"""
import asyncio
import functools
import logging
from typing import Any, Callable, Type, TypeVar
logger = logging.getLogger(__name__)
T = TypeVar("T")
class RetryConfig:
"""Retry configuration class"""
def __init__(
self,
enabled: bool = True,
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 60.0,
exponential_base: float = 2.0,
retryable_exceptions: tuple[Type[Exception], ...] = (Exception,),
):
"""
Args:
enabled: Whether to enable retry mechanism
max_retries: Maximum number of retries
initial_delay: Initial delay time (seconds)
max_delay: Maximum delay time (seconds)
exponential_base: Exponential backoff base
retryable_exceptions: Tuple of retryable exception types
"""
self.enabled = enabled
self.max_retries = max_retries
self.initial_delay = initial_delay
self.max_delay = max_delay
self.exponential_base = exponential_base
self.retryable_exceptions = retryable_exceptions
def calculate_delay(self, attempt: int) -> float:
"""Calculate delay time (exponential backoff)
Args:
attempt: Current attempt number (starting from 0)
Returns:
Delay time (seconds)
"""
delay = self.initial_delay * (self.exponential_base**attempt)
return min(delay, self.max_delay)
class RetryExhaustedError(Exception):
"""Retry exhausted exception"""
def __init__(self, last_exception: Exception, attempts: int):
self.last_exception = last_exception
self.attempts = attempts
super().__init__(f"Retry failed after {attempts} attempts. Last error: {str(last_exception)}")
def async_retry(
config: RetryConfig | None = None,
on_retry: Callable[[Exception, int], None] | None = None,
) -> Callable:
"""Async function retry decorator
Args:
config: Retry configuration object, uses default config if None
on_retry: Callback function on retry, receives exception and current attempt number
Returns:
Decorator function
Example:
```python
@async_retry(RetryConfig(max_retries=3, initial_delay=1.0))
async def call_api():
# API call code
pass
```
"""
if config is None:
config = RetryConfig()
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exception: Exception | None = None
for attempt in range(config.max_retries + 1):
try:
# Try to execute function
return await func(*args, **kwargs)
except config.retryable_exceptions as e:
last_exception = e
# If this is the last attempt, don't retry
if attempt >= config.max_retries:
logger.error(f"Function {func.__name__} retry failed, reached maximum retry count {config.max_retries}")
raise RetryExhaustedError(e, attempt + 1)
# Calculate delay time
delay = config.calculate_delay(attempt)
# Log
logger.warning(
f"Function {func.__name__} call {attempt + 1} failed: {str(e)}, "
f"retrying attempt {attempt + 2} after {delay:.2f} seconds"
)
# Call callback function
if on_retry:
on_retry(e, attempt + 1)
# Wait before retry
await asyncio.sleep(delay)
# Should not reach here in theory
if last_exception:
raise last_exception
raise Exception("Unknown error")
return wrapper
return decorator