"""Module de gestion de la résilience réseau (Retry & Exponential Backoff). Fournit un système robuste de ré-essais automatiques avec croissance exponentielle du délai et intégration de bruit aléatoire (Jitter) pour résister aux rate-limits (HTTP 429) et timeouts API. """ import random import time from typing import Callable, Optional class MaxRetriesExceededError(Exception): """Exception levée lorsque le nombre maximal de ré-essais autorisés a été atteint sans succès.""" pass def calculate_backoff_delay( attempt: int, base_delay: float = 1.0, backoff_factor: float = 2.0, max_delay: float = 60.0, jitter: bool = True, ) -> float: """Calcule le temps d'attente exponentiel avec Jitter (bruit aléatoire) pour un essai donné. Formule: delay = min(max_delay, base_delay * (backoff_factor ** attempt)) + jitter Args: attempt (int): Numéro de la tentative de ré-essai courant (0-indexed). base_delay (float): Délai initial en secondes (défaut: 1.0s). backoff_factor (float): Multiplicateur exponentiel (défaut: 2.0). max_delay (float): Plafond maximal de délai en secondes (défaut: 60.0s). jitter (bool): Si True, ajoute un délai aléatoire (0.0..0.5s) pour échelonner les appels concurrents. Returns: float: Le temps d'attente calculé en secondes (arrondi à 3 décimales). """ calculated = base_delay * (backoff_factor ** attempt) delay = min(max_delay, calculated) if jitter: delay += random.uniform(0.0, 0.5) return round(delay, 3) def retry_with_exponential_backoff( fn: Optional[Callable] = None, max_retries: int = 3, base_delay: float = 1.0, backoff_factor: float = 2.0, max_delay: float = 60.0, jitter: bool = True, retryable_exceptions: tuple[type[Exception], ...] = (Exception,), on_retry_callback: Optional[Callable[[int, Exception, float], None]] = None, ): """Décorateur et wrapper exécutant des ré-essais automatiques avec Exponential Backoff & Jitter. Args: fn (Optional[Callable]): La fonction à exécuter ou décorer. max_retries (int): Nombre maximal de ré-essais autorisés (défaut: 3). base_delay (float): Délai initial avant le premier ré-essai (défaut: 1.0s). backoff_factor (float): Facteur de multiplication exponentiel (défaut: 2.0). max_delay (float): Temps d'attente maximal entre deux tentatives (défaut: 60.0s). jitter (bool): Ajoute une variation aléatoire pour désynchroniser les ré-essais. retryable_exceptions (tuple): Tuple de classes d'exceptions autorisant un ré-essai. on_retry_callback (Optional[Callable]): Callback appelé à chaque échec avec (attempt_count, error, delay). Returns: Callable: La fonction décorée gérant la tolérance aux pannes. Raises: MaxRetriesExceededError: Si le quota max_retries est dépassé. """ def decorator(func: Callable) -> Callable: def wrapper(*args, **kwargs): attempts = 0 while True: try: return func(*args, **kwargs) except retryable_exceptions as error: from src.llm_client import APIKeyMissingError if isinstance(error, APIKeyMissingError): raise error attempts += 1 if attempts > max_retries: raise MaxRetriesExceededError( f"Échec après {max_retries + 1} tentatives (max_retries={max_retries}). " f"Dernière erreur : {error}" ) from error delay = calculate_backoff_delay( attempt=attempts - 1, base_delay=base_delay, backoff_factor=backoff_factor, max_delay=max_delay, jitter=jitter, ) if on_retry_callback: on_retry_callback(attempts, error, delay) time.sleep(delay) return wrapper if fn is None: return decorator return decorator(fn)