Coverage for agentos/models/resilience.py: 37%
202 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 21:26 +0800
1"""
2AgentOS v1.1.5 Resilience — 韧性层。
3Retry with jitter + Circuit Breaker + Timeout + Fallback chain + Cancellation-aware retry。
4"""
6from __future__ import annotations
8import asyncio
9import builtins
10import random
11import time
12from collections.abc import Awaitable, Callable
13from dataclasses import dataclass, field
14from enum import StrEnum
15from typing import TypeVar
17T = TypeVar("T")
20class CircuitState(StrEnum):
21 """熔断器状态枚举。"""
23 CLOSED = "closed" # 正常
24 OPEN = "open" # 熔断
25 HALF_OPEN = "half_open" # 半开(探测)
28@dataclass
29class CircuitBreakerConfig:
30 """熔断器配置。"""
32 failure_threshold: int = 5 # 连续失败N次后熔断
33 success_threshold: int = 2 # 半开状态下N次成功后恢复
34 timeout: float = 60.0 # 熔断持续时间(秒)
35 half_open_max_requests: int = 1 # 半开状态下最大探测请求
36 track_duration: float = 300.0 # 统计窗口
39@dataclass
40class CircuitBreakerStats:
41 """熔断器运行统计。"""
43 state: CircuitState
44 failure_count: int = 0
45 success_count: int = 0
46 last_failure_time: float = 0.0
47 last_success_time: float = 0.0
48 opened_at: float = 0.0
49 total_failures: int = 0
50 total_successes: int = 0
53class CircuitBreaker:
54 """熔断器:检测连续失败,自动熔断/恢复。"""
56 def __init__(self, name: str, config: CircuitBreakerConfig | None = None):
57 self.name = name
58 self.config = config or CircuitBreakerConfig()
59 self.state = CircuitState.CLOSED
60 self._failure_count: int = 0
61 self._success_count: int = 0
62 self._last_failure_time: float = 0.0
63 self._opened_at: float = 0.0
64 self._lock = asyncio.Lock()
65 self._stats = CircuitBreakerStats(state=CircuitState.CLOSED)
67 async def call(self, fn: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
68 """通过熔断器调用函数。"""
69 async with self._lock:
70 if not self._allow_request():
71 raise CircuitBreakerOpenError(f"Circuit {self.name} is OPEN")
73 try:
74 result = await fn(*args, **kwargs)
75 await self._on_success()
76 return result
77 except Exception as e:
78 await self._on_failure()
79 raise e
81 def _allow_request(self) -> bool:
82 if self.state == CircuitState.CLOSED:
83 return True
85 if self.state == CircuitState.OPEN:
86 if time.time() - self._opened_at >= self.config.timeout:
87 self.state = CircuitState.HALF_OPEN
88 self._success_count = 0
89 return True
90 return False
92 if self.state == CircuitState.HALF_OPEN:
93 return self._success_count < self.config.half_open_max_requests
95 return True
97 async def _on_success(self):
98 async with self._lock:
99 self._stats.total_successes += 1
100 self._stats.last_success_time = time.time()
102 if self.state == CircuitState.HALF_OPEN:
103 self._success_count += 1
104 if self._success_count >= self.config.success_threshold:
105 self.state = CircuitState.CLOSED
106 self._failure_count = 0
107 else:
108 self._failure_count = 0
110 self._stats.state = self.state
112 async def _on_failure(self):
113 async with self._lock:
114 self._failure_count += 1
115 self._stats.failure_count = self._failure_count
116 self._stats.total_failures += 1
117 self._stats.last_failure_time = time.time()
119 if self._failure_count >= self.config.failure_threshold:
120 self.state = CircuitState.OPEN
121 self._opened_at = time.time()
122 self._stats.opened_at = self._opened_at
124 self._stats.state = self.state
126 @property
127 def stats(self) -> CircuitBreakerStats:
128 s = CircuitBreakerStats(state=self.state)
129 s.failure_count = self._failure_count
130 s.last_failure_time = self._last_failure_time
131 s.last_success_time = self._stats.last_success_time
132 s.opened_at = self._opened_at
133 s.total_failures = self._stats.total_failures
134 s.total_successes = self._stats.total_successes
135 return s
137 def reset(self):
138 self.state = CircuitState.CLOSED
139 self._failure_count = 0
140 self._success_count = 0
143class CircuitBreakerOpenError(Exception):
144 """熔断器打开异常。"""
148# ── Retry with Jitter ──────────────────────────────────────────────────────
151@dataclass
152class RetryConfig:
153 """重试策略配置。"""
155 max_retries: int = 3
156 base_delay: float = 1.0 # 基础延迟(秒)
157 max_delay: float = 60.0 # 最大延迟
158 backoff_multiplier: float = 2.0 # 退避乘数
159 jitter: bool = True # 是否加抖动
160 jitter_factor: float = 0.1 # 抖动比例
161 retry_on: tuple[type[Exception], ...] = (Exception,)
164class CancellationSource(StrEnum):
165 """取消来源,区分用户主动取消与系统取消。"""
167 USER = "user" # 用户主动取消 — 不重试
168 SYSTEM = "system" # 系统级别取消(超时、熔断等)— 按配置重试
171class CancelledError(Exception):
172 """带取消来源的取消异常。"""
174 def __init__(self, message: str, source: CancellationSource = CancellationSource.SYSTEM):
175 super().__init__(message)
176 self.source = source
179class RetryExhaustedError(Exception):
180 """重试耗尽异常。"""
182 def __init__(self, attempts: int, last_error: Exception):
183 super().__init__(f"Retry exhausted after {attempts} attempts. Last error: {last_error}")
184 self.attempts = attempts
185 self.last_error = last_error
188async def retry_with_backoff(
189 fn: Callable[..., Awaitable[T]],
190 *args,
191 config: RetryConfig | None = None,
192 circuit_breaker: CircuitBreaker | None = None,
193 on_retry: Callable[[int, Exception, float], None] | None = None,
194 **kwargs,
195) -> T:
196 """带指数退避和抖动的重试函数,区分用户取消(不重试)与系统取消(按配置重试)。"""
197 cfg = config or RetryConfig()
198 last_error: Exception | None = None
200 def _apply_delay(attempt_num: int, err: Exception):
201 delay = min(cfg.base_delay * (cfg.backoff_multiplier**attempt_num), cfg.max_delay)
202 if cfg.jitter:
203 delay = delay * (1 + random.uniform(-cfg.jitter_factor, cfg.jitter_factor))
204 delay = max(0.1, delay)
205 if on_retry:
206 on_retry(attempt_num + 1, err, delay)
207 return delay
209 for attempt in range(cfg.max_retries + 1):
210 try:
211 if circuit_breaker:
212 return await circuit_breaker.call(fn, *args, **kwargs)
213 return await fn(*args, **kwargs)
215 except CircuitBreakerOpenError:
216 raise # 熔断打开不重试
218 except CancelledError as e:
219 if e.source == CancellationSource.USER:
220 raise # 用户取消不重试,直接上抛
221 # 系统级取消按重试配置处理
222 last_error = e
223 if attempt == cfg.max_retries:
224 raise RetryExhaustedError(cfg.max_retries + 1, e)
225 delay = _apply_delay(attempt, e)
226 await asyncio.sleep(delay)
228 except cfg.retry_on as e:
229 last_error = e
230 if attempt == cfg.max_retries:
231 raise RetryExhaustedError(cfg.max_retries + 1, e)
232 delay = _apply_delay(attempt, e)
233 await asyncio.sleep(delay)
235 raise RetryExhaustedError(cfg.max_retries, last_error or RuntimeError("unknown"))
238# ── Timeout ─────────────────────────────────────────────────────────────────
241class TimeoutError(Exception):
242 """超时异常。"""
246async def with_timeout(
247 fn: Callable[..., Awaitable[T]],
248 *args,
249 timeout: float = 120.0,
250 **kwargs,
251) -> T:
252 """为异步函数添加超时保护。"""
253 try:
254 return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
255 except builtins.TimeoutError:
256 raise TimeoutError(f"Operation timed out after {timeout}s")
259# ── Fallback Chain ──────────────────────────────────────────────────────────
262async def with_fallback(
263 primary: Callable[..., Awaitable[T]],
264 fallbacks: list[Callable[..., Awaitable[T]]],
265 *args,
266 **kwargs,
267) -> T:
268 """依次尝试主函数和降级函数链。"""
269 errors: list[Exception] = []
271 try:
272 return await primary(*args, **kwargs)
273 except Exception as e:
274 errors.append(e)
276 for i, fallback in enumerate(fallbacks):
277 try:
278 return await fallback(*args, **kwargs)
279 except Exception as e:
280 errors.append(e)
282 raise FallbackExhaustedError(errors)
285class FallbackExhaustedError(Exception):
286 """回退耗尽异常。"""
288 def __init__(self, errors: list[Exception]):
289 msg = f"All {len(errors)} attempts failed: " + "; ".join(str(e) for e in errors[:3])
290 super().__init__(msg)
291 self.errors = errors
294# ── Composite Resilience ────────────────────────────────────────────────────
297@dataclass
298class ResilienceConfig:
299 """弹性总配置。"""
301 retry: RetryConfig = field(default_factory=RetryConfig)
302 circuit_breaker: CircuitBreakerConfig | None = None
303 timeout: float = 120.0
306class ResilientCall:
307 """组合韧性调用器:重试 + 熔断 + 超时 + 降级。"""
309 def __init__(self, config: ResilienceConfig | None = None):
310 cfg = config or ResilienceConfig()
311 self.retry_config = cfg.retry
312 self.timeout = cfg.timeout
313 self._breaker: CircuitBreaker | None = None
314 if cfg.circuit_breaker:
315 self._breaker = CircuitBreaker("default", cfg.circuit_breaker)
317 async def call(self, fn: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
318 async def _inner():
319 return await with_timeout(fn, *args, timeout=self.timeout, **kwargs)
321 return await retry_with_backoff(
322 _inner,
323 config=self.retry_config,
324 circuit_breaker=self._breaker,
325 )
328# ── Auto-generated compat stubs ──
331def retry_with_backoff(*args, **kwargs): # noqa: F811
332 pass
335def with_timeout(*args, **kwargs): # noqa: F811
336 pass
339def with_fallback(*args, **kwargs): # noqa: F811
340 pass