refactor(infra): 调整导入顺序并为熔断器添加事件回调功能

1. 调整text_chunker.py中导入包的顺序
2. 调整config_watcher.py的typing导入顺序
3. 重构__init__.py的导入排序
4. 为CircuitBreaker新增事件回调机制,支持状态变化通知
This commit is contained in:
Kris 2026-05-13 16:19:28 +08:00
parent b2ea3426fd
commit 6cf9c8e9b6
4 changed files with 34 additions and 3 deletions

View File

@ -1,6 +1,6 @@
from yuxi.channels.infra.broadcast import EventBroadcaster
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError, CircuitState from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError, CircuitState
from yuxi.channels.infra.config_watcher import ConfigWatcher from yuxi.channels.infra.config_watcher import ConfigWatcher
from yuxi.channels.infra.broadcast import EventBroadcaster
from yuxi.channels.infra.external_process import ExternalProcessManager from yuxi.channels.infra.external_process import ExternalProcessManager
from yuxi.channels.infra.text_chunker import ChunkMode, TextChunk, TextChunker from yuxi.channels.infra.text_chunker import ChunkMode, TextChunk, TextChunker

View File

@ -19,16 +19,27 @@ class CircuitBreakerOpenError(Exception):
pass pass
CircuitBreakerEventCallback = (
Callable[[str, str, dict[str, Any]], Awaitable[None]] | Callable[[str, str, dict[str, Any]], None] | None
)
class CircuitBreaker: class CircuitBreaker:
def __init__( def __init__(
self, self,
failure_threshold: int = 5, failure_threshold: int = 5,
recovery_timeout: float = 60.0, recovery_timeout: float = 60.0,
half_open_max_calls: int = 3, half_open_max_calls: int = 3,
channel_id: str = "",
operation: str = "default",
on_event: CircuitBreakerEventCallback = None,
): ):
self.failure_threshold = failure_threshold self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout self.recovery_timeout = recovery_timeout
self.half_open_max_calls = half_open_max_calls self.half_open_max_calls = half_open_max_calls
self.channel_id = channel_id
self.operation = operation
self._on_event = on_event
self.state = CircuitState.CLOSED self.state = CircuitState.CLOSED
self._failure_count = 0 self._failure_count = 0
@ -44,6 +55,7 @@ class CircuitBreaker:
self.state = CircuitState.HALF_OPEN self.state = CircuitState.HALF_OPEN
self._half_open_calls = 0 self._half_open_calls = 0
logger.info("Circuit breaker transitioned to HALF_OPEN") logger.info("Circuit breaker transitioned to HALF_OPEN")
await self._emit_event("on_half_open", {})
else: else:
raise CircuitBreakerOpenError( raise CircuitBreakerOpenError(
f"Circuit breaker is OPEN, retry after {self.recovery_timeout - elapsed:.0f}s" f"Circuit breaker is OPEN, retry after {self.recovery_timeout - elapsed:.0f}s"
@ -69,6 +81,7 @@ class CircuitBreaker:
self._failure_count = 0 self._failure_count = 0
self._half_open_calls = 0 self._half_open_calls = 0
logger.info("Circuit breaker closed after successful HALF_OPEN probe") logger.info("Circuit breaker closed after successful HALF_OPEN probe")
await self._emit_event("on_close", {})
elif self.state == CircuitState.CLOSED and self._failure_count > 0: elif self.state == CircuitState.CLOSED and self._failure_count > 0:
self._failure_count = 0 self._failure_count = 0
@ -82,6 +95,24 @@ class CircuitBreaker:
logger.warning( logger.warning(
f"Circuit breaker OPEN after {self._failure_count} failures (threshold={self.failure_threshold})" f"Circuit breaker OPEN after {self._failure_count} failures (threshold={self.failure_threshold})"
) )
await self._emit_event("on_open", {"failure_count": self._failure_count})
elif self.state == CircuitState.HALF_OPEN: elif self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.OPEN self.state = CircuitState.OPEN
logger.warning("Circuit breaker re-OPENed after HALF_OPEN failure") logger.warning("Circuit breaker re-OPENed after HALF_OPEN failure")
await self._emit_event("on_open", {"failure_count": self._failure_count, "from": "half_open"})
async def _emit_event(self, event: str, detail: dict[str, Any]) -> None:
if self._on_event is None:
return
payload = {
"channel_id": self.channel_id,
"operation": self.operation,
"state": self.state,
**detail,
}
try:
result = self._on_event(event, self.channel_id, payload) # type: ignore[call-arg]
if asyncio.iscoroutine(result):
await result
except Exception:
logger.debug(f"CircuitBreaker event callback error: {event}", exc_info=True)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import hashlib import hashlib
import json import json
from typing import Any, TYPE_CHECKING from typing import TYPE_CHECKING, Any
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger

View File

@ -2,8 +2,8 @@ from __future__ import annotations
import re import re
from collections.abc import Iterator from collections.abc import Iterator
from enum import StrEnum
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum
class ChunkMode(StrEnum): class ChunkMode(StrEnum):