import asyncio import logging import time from dataclasses import dataclass, field from enum import StrEnum logger = logging.getLogger(__name__) class CircuitState(StrEnum): CLOSED = "closed" OPEN = "open" HALF_OPEN = "half_open" @dataclass(slots=True) class CircuitBreakerConfig: failure_threshold: int = 5 recovery_timeout_sec: float = 30.0 half_open_max_requests: int = 1 consecutive_successes_to_close: int = 2 @dataclass class CircuitBreaker: """简单熔断器。 状态机: - CLOSED: 正常通行,累计失败计数。 - OPEN: 拒绝请求,持续 recovery_timeout_sec 后转 HALF_OPEN。 - HALF_OPEN: 允许少量探测请求。连续成功则转 CLOSED,任何失败则回 OPEN。 """ name: str config: CircuitBreakerConfig = field(default_factory=CircuitBreakerConfig) _state: CircuitState = CircuitState.CLOSED _failure_count: int = 0 _success_count: int = 0 _last_failure_time: float = 0.0 _lock: asyncio.Lock = field(default_factory=asyncio.Lock) async def call(self, coro) -> object: if not await self._allow_request(): raise CircuitBreakerOpenError( f"Circuit breaker '{self.name}' is OPEN" ) try: result = await coro except Exception: await self._record_failure() raise else: await self._record_success() return result async def _allow_request(self) -> bool: async with self._lock: if self._state == CircuitState.CLOSED: return True if self._state == CircuitState.OPEN: if time.monotonic() - self._last_failure_time >= self.config.recovery_timeout_sec: self._state = CircuitState.HALF_OPEN self._success_count = 0 logger.info("Circuit '%s' transitioning OPEN -> HALF_OPEN", self.name) return True return False if self._state == CircuitState.HALF_OPEN: return True return True async def _record_success(self) -> None: async with self._lock: if self._state == CircuitState.HALF_OPEN: self._success_count += 1 if self._success_count >= self.config.consecutive_successes_to_close: self._state = CircuitState.CLOSED self._failure_count = 0 self._success_count = 0 logger.info("Circuit '%s' transitioning HALF_OPEN -> CLOSED", self.name) async def _record_failure(self) -> None: async with self._lock: self._failure_count += 1 self._last_failure_time = time.monotonic() if self._state == CircuitState.HALF_OPEN: self._state = CircuitState.OPEN logger.warning( "Circuit '%s' transitioning HALF_OPEN -> OPEN (probe failed)", self.name, ) elif self._state == CircuitState.CLOSED and self._failure_count >= self.config.failure_threshold: self._state = CircuitState.OPEN logger.warning( "Circuit '%s' transitioning CLOSED -> OPEN after %d failures", self.name, self._failure_count, ) @property def state(self) -> CircuitState: return self._state async def reset(self) -> None: async with self._lock: self._state = CircuitState.CLOSED self._failure_count = 0 self._success_count = 0 class CircuitBreakerOpenError(Exception): pass