import asyncio from enum import Enum from datetime import datetime, timedelta import logging from typing import Optional, Callable, Any from utils.logging_utils import log_to_file class CircuitState(Enum): CLOSED = "CLOSED" # Circuit fermé - Opérations normales OPEN = "OPEN" # Circuit ouvert - Échecs détectés HALF_OPEN = "HALF_OPEN" # Circuit semi-ouvert - Test de récupération class AsyncCircuitBreaker: def __init__( self, failure_threshold: int = 5, recovery_timeout: int = 60, half_open_timeout: int = 30 ): self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout self.half_open_timeout = half_open_timeout self.state = CircuitState.CLOSED self.failure_count = 0 self.last_failure_time: Optional[datetime] = None self.lock = asyncio.Lock() async def call(self, func: Callable, *args, **kwargs) -> Any: """Exécute la fonction avec la logique du circuit breaker""" async with self.lock: if self.state == CircuitState.OPEN: if self._should_attempt_recovery(): self.state = CircuitState.HALF_OPEN log_to_file("Circuit breaker passé en état HALF_OPEN", level=logging.INFO) else: raise Exception("Circuit breaker ouvert - Service indisponible") try: result = await func(*args, **kwargs) if self.state == CircuitState.HALF_OPEN: self._reset() return result except Exception as e: await self._handle_failure(e) raise def _should_attempt_recovery(self) -> bool: """Vérifie si on doit tenter une récupération""" if not self.last_failure_time: return True recovery_time = self.last_failure_time + timedelta(seconds=self.recovery_timeout) return datetime.now() > recovery_time async def _handle_failure(self, exception: Exception) -> None: """Gère un échec d'appel""" self.failure_count += 1 self.last_failure_time = datetime.now() if self.state == CircuitState.HALF_OPEN or self.failure_count >= self.failure_threshold: self.state = CircuitState.OPEN log_to_file(f"Circuit breaker ouvert après {self.failure_count} échecs", level=logging.WARNING) def _reset(self) -> None: """Réinitialise le circuit breaker""" self.state = CircuitState.CLOSED self.failure_count = 0 self.last_failure_time = None log_to_file("Circuit breaker réinitialisé et fermé", level=logging.INFO) def get_state(self) -> CircuitState: """Retourne l'état actuel du circuit breaker""" return self.state