ForcePilot/backend/package/yuxi/channel/message/circuit_breaker.py
Kris 0b19470c70 feat(channel/message): 新增消息处理核心模块及相关工具类
本次提交新增了渠道消息处理的完整核心模块,包含以下核心功能:
1.  新增会话围栏类,实现会话并发控制与过期清理
2.  新增媒体清理器,实现过期媒体文件自动清理
3.  新增熔断器组件,实现服务降级与故障隔离
4.  新增消息处理器,完成渠道消息的完整流转处理
5.  新增限流器组件,实现渠道级和账户级流量控制
6.  新增链路追踪模块,集成Langfuse实现调用链路监控
7.  新增指标统计模块,实现消息处理全链路指标采集
8.  新增统一消息模型,封装全渠道消息格式
9.  新增块回复流水线,实现流式回复的合并与去重
10. 新增本地媒体存储模块,实现媒体文件的本地管理
11. 新增回复分发器,实现回复内容的有序发送与延迟处理
12. 完善__init__.py导出所有核心模块与工具类
2026-05-21 10:27:16 +08:00

112 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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