ForcePilot/backend/package/yuxi/channels/infra/circuit_breaker.py
Kris 7a972055b7 feat: 新增渠道服务基础框架与核心工具类
新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括:
1.  多协议定义:认证、消息、配置、网关等核心接口
2.  策略模块:上下文、群聊、去重、防抖等业务策略
3.  工具集:重试、去重、文本分块、消息格式化等SDK工具
4.  基础设施:外部进程管理、事件广播、熔断机制等
5.  账户与管道系统:账户管理、消息处理管道实现
6.  运行时服务:状态收集、维护任务、日志等后台服务
2026-05-12 00:53:57 +08:00

88 lines
3.1 KiB
Python

from __future__ import annotations
import asyncio
import time
from collections.abc import Awaitable, Callable
from enum import StrEnum
from typing import Any
from yuxi.utils.logging_config import logger
class CircuitState(StrEnum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
class CircuitBreakerOpenError(Exception):
pass
class CircuitBreaker:
def __init__(
self,
failure_threshold: int = 5,
recovery_timeout: float = 60.0,
half_open_max_calls: int = 3,
):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.half_open_max_calls = half_open_max_calls
self.state = CircuitState.CLOSED
self._failure_count = 0
self._last_failure_time: float = 0.0
self._half_open_calls = 0
self._lock = asyncio.Lock()
async def call(self, func: Callable[[], Awaitable[Any]]) -> Any:
async with self._lock:
if self.state == CircuitState.OPEN:
elapsed = time.monotonic() - self._last_failure_time
if elapsed >= self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
self._half_open_calls = 0
logger.info("Circuit breaker transitioned to HALF_OPEN")
else:
raise CircuitBreakerOpenError(
f"Circuit breaker is OPEN, retry after {self.recovery_timeout - elapsed:.0f}s"
)
if self.state == CircuitState.HALF_OPEN:
if self._half_open_calls >= self.half_open_max_calls:
raise CircuitBreakerOpenError("Circuit breaker HALF_OPEN max calls reached")
self._half_open_calls += 1
try:
result = await func()
await self.record_success()
return result
except Exception:
await self.record_failure()
raise
async def record_success(self) -> None:
async with self._lock:
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED
self._failure_count = 0
self._half_open_calls = 0
logger.info("Circuit breaker closed after successful HALF_OPEN probe")
elif self.state == CircuitState.CLOSED and self._failure_count > 0:
self._failure_count = 0
async def record_failure(self) -> None:
async with self._lock:
self._failure_count += 1
self._last_failure_time = time.monotonic()
if self.state == CircuitState.CLOSED and self._failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
logger.warning(
f"Circuit breaker OPEN after {self._failure_count} failures (threshold={self.failure_threshold})"
)
elif self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.OPEN
logger.warning("Circuit breaker re-OPENed after HALF_OPEN failure")