""" Retry utilities for shared use across services """ import asyncio import random from typing import Callable, Any, Tuple, Type import logging logger = logging.getLogger(__name__) async def retry_with_backoff( func, max_retries: int = 3, exceptions: Tuple[Type[Exception], ...] = (Exception,), base_delay: float = 1.0, max_delay: float = 60.0, backoff_factor: float = 2.0 ): """ Retry a function with exponential backoff. Args: func: The function to retry (can be sync or async) max_retries: Maximum number of retry attempts exceptions: Tuple of exception types to catch and retry base_delay: Initial delay in seconds max_delay: Maximum delay between retries backoff_factor: Factor by which delay increases after each retry Returns: Result of the function call Raises: The original exception if all retries are exhausted """ for attempt in range(max_retries + 1): # +1 because first attempt doesn't count as retry try: result = func() # Handle both async functions and lambdas that return coroutines if asyncio.iscoroutine(result): result = await result return result except exceptions as e: if attempt == max_retries: # Exhausted all retries, re-raise the exception raise e # Calculate delay with exponential backoff and jitter delay = min(base_delay * (backoff_factor ** attempt), max_delay) # Add jitter to prevent thundering herd delay = delay * (0.5 + random.random() * 0.5) logger.warning( f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s: {str(e)}", extra={ "attempt": attempt + 1, "max_retries": max_retries, "exception": str(e) } ) await asyncio.sleep(delay)