File size: 2,889 Bytes
fe4792e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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