# ============================================ # Projet : API-KA # Fichier : src/utils/retry.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Relance avec backoff exponentiel : 3 tentatives, délais 30s → 2min → 10min.""" from __future__ import annotations import functools import time from collections.abc import Callable, Sequence from typing import Any, TypeVar from src.utils.logger import get_logger T = TypeVar("T") DEFAULT_ATTEMPTS = 3 DEFAULT_DELAYS: tuple[float, ...] = (30.0, 120.0, 600.0) def retry_call( func: Callable[[], T], *, attempts: int = DEFAULT_ATTEMPTS, delays: Sequence[float] = DEFAULT_DELAYS, on_retry: Callable[[int, BaseException, float], None] | None = None, ) -> T: """Exécute ``func`` avec jusqu'à ``attempts`` relances après l'essai initial. Args: func: Callable sans argument à exécuter. attempts: Nombre maximal de relances après le premier échec. delays: Délais (secondes) avant chaque relance ; le dernier est réutilisé si ``attempts`` dépasse la longueur de la séquence. on_retry: Callback ``(numéro_de_relance, exception, délai)`` appelé avant chaque relance. Returns: Le résultat de ``func``. Raises: BaseException: La dernière exception si toutes les tentatives échouent. """ logger = get_logger() for attempt in range(attempts + 1): try: return func() except Exception as exc: if attempt >= attempts: raise delay = float(delays[min(attempt, len(delays) - 1)]) if delays else 0.0 logger.warning( "Échec, relance planifiée", extra={ "attempt": attempt + 1, "delay_seconds": delay, "error": str(exc), }, ) if on_retry is not None: on_retry(attempt + 1, exc, delay) if delay > 0: time.sleep(delay) raise RuntimeError("retry_call: état inatteignable") # pragma: no cover def retry( attempts: int = DEFAULT_ATTEMPTS, delays: Sequence[float] = DEFAULT_DELAYS, ) -> Callable[[Callable[..., T]], Callable[..., T]]: """Décorateur appliquant :func:`retry_call` à la fonction décorée.""" def decorator(func: Callable[..., T]) -> Callable[..., T]: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> T: return retry_call( lambda: func(*args, **kwargs), attempts=attempts, delays=delays ) return wrapper return decorator