Coverage for agentos/models/resilience.py: 37%
199 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +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 random
10import time
11from dataclasses import dataclass, field
12from enum import Enum
13from typing import Callable, Awaitable, TypeVar
15T = TypeVar("T")
18class CircuitState(str, Enum):
20 """熔断器状态枚举。"""
22 CLOSED = "closed" # 正常
23 OPEN = "open" # 熔断
24 HALF_OPEN = "half_open" # 半开(探测)
27@dataclass
28class CircuitBreakerConfig:
29 """熔断器配置。"""
31 failure_threshold: int = 5 # 连续失败N次后熔断
32 success_threshold: int = 2 # 半开状态下N次成功后恢复
33 timeout: float = 60.0 # 熔断持续时间(秒)
34 half_open_max_requests: int = 1 # 半开状态下最大探测请求
35 track_duration: float = 300.0 # 统计窗口
38@dataclass
39class CircuitBreakerStats:
40 """熔断器运行统计。"""
42 state: CircuitState
43 failure_count: int = 0
44 success_count: int = 0
45 last_failure_time: float = 0.0
46 last_success_time: float = 0.0
47 opened_at: float = 0.0
48 total_failures: int = 0
49 total_successes: int = 0
52class CircuitBreaker:
53 """熔断器:检测连续失败,自动熔断/恢复。"""
55 def __init__(self, name: str, config: CircuitBreakerConfig | None = None):
56 self.name = name
57 self.config = config or CircuitBreakerConfig()
58 self.state = CircuitState.CLOSED
59 self._failure_count: int = 0
60 self._success_count: int = 0
61 self._last_failure_time: float = 0.0
62 self._opened_at: float = 0.0
63 self._lock = asyncio.Lock()
64 self._stats = CircuitBreakerStats(state=CircuitState.CLOSED)
66 async def call(self, fn: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
67 """通过熔断器调用函数。"""
68 async with self._lock:
69 if not self._allow_request():
70 raise CircuitBreakerOpenError(f"Circuit {self.name} is OPEN")
72 try:
73 result = await fn(*args, **kwargs)
74 await self._on_success()
75 return result
76 except Exception as e:
77 await self._on_failure()
78 raise e
80 def _allow_request(self) -> bool:
81 if self.state == CircuitState.CLOSED:
82 return True
84 if self.state == CircuitState.OPEN:
85 if time.time() - self._opened_at >= self.config.timeout:
86 self.state = CircuitState.HALF_OPEN
87 self._success_count = 0
88 return True
89 return False
91 if self.state == CircuitState.HALF_OPEN:
92 return self._success_count < self.config.half_open_max_requests
94 return True
96 async def _on_success(self):
97 async with self._lock:
98 self._stats.total_successes += 1
99 self._stats.last_success_time = time.time()
101 if self.state == CircuitState.HALF_OPEN:
102 self._success_count += 1
103 if self._success_count >= self.config.success_threshold:
104 self.state = CircuitState.CLOSED
105 self._failure_count = 0
106 else:
107 self._failure_count = 0
109 self._stats.state = self.state
111 async def _on_failure(self):
112 async with self._lock:
113 self._failure_count += 1
114 self._stats.failure_count = self._failure_count
115 self._stats.total_failures += 1
116 self._stats.last_failure_time = time.time()
118 if self._failure_count >= self.config.failure_threshold:
119 self.state = CircuitState.OPEN
120 self._opened_at = time.time()
121 self._stats.opened_at = self._opened_at
123 self._stats.state = self.state
125 @property
126 def stats(self) -> CircuitBreakerStats:
127 s = CircuitBreakerStats(state=self.state)
128 s.failure_count = self._failure_count
129 s.last_failure_time = self._last_failure_time
130 s.last_success_time = self._stats.last_success_time
131 s.opened_at = self._opened_at
132 s.total_failures = self._stats.total_failures
133 s.total_successes = self._stats.total_successes
134 return s
136 def reset(self):
137 self.state = CircuitState.CLOSED
138 self._failure_count = 0
139 self._success_count = 0
142class CircuitBreakerOpenError(Exception):
143 """熔断器打开异常。"""
144 pass
147# ── Retry with Jitter ──────────────────────────────────────────────────────
149@dataclass
150class RetryConfig:
151 """重试策略配置。"""
152 max_retries: int = 3
153 base_delay: float = 1.0 # 基础延迟(秒)
154 max_delay: float = 60.0 # 最大延迟
155 backoff_multiplier: float = 2.0 # 退避乘数
156 jitter: bool = True # 是否加抖动
157 jitter_factor: float = 0.1 # 抖动比例
158 retry_on: tuple[type[Exception], ...] = (Exception,)
161class CancellationSource(str, Enum):
162 """取消来源,区分用户主动取消与系统取消。"""
164 USER = "user" # 用户主动取消 — 不重试
165 SYSTEM = "system" # 系统级别取消(超时、熔断等)— 按配置重试
168class CancelledError(Exception):
169 """带取消来源的取消异常。"""
171 def __init__(self, message: str, source: CancellationSource = CancellationSource.SYSTEM):
172 super().__init__(message)
173 self.source = source
176class RetryExhaustedError(Exception):
177 """重试耗尽异常。"""
179 def __init__(self, attempts: int, last_error: Exception):
180 super().__init__(f"Retry exhausted after {attempts} attempts. Last error: {last_error}")
181 self.attempts = attempts
182 self.last_error = last_error
185async def retry_with_backoff(
186 fn: Callable[..., Awaitable[T]],
187 *args,
188 config: RetryConfig | None = None,
189 circuit_breaker: CircuitBreaker | None = None,
190 on_retry: Callable[[int, Exception, float], None] | None = None,
191 **kwargs,
192) -> T:
193 """带指数退避和抖动的重试函数,区分用户取消(不重试)与系统取消(按配置重试)。"""
194 cfg = config or RetryConfig()
195 last_error: Exception | None = None
197 def _apply_delay(attempt_num: int, err: Exception):
198 delay = min(cfg.base_delay * (cfg.backoff_multiplier ** attempt_num), cfg.max_delay)
199 if cfg.jitter:
200 delay = delay * (1 + random.uniform(-cfg.jitter_factor, cfg.jitter_factor))
201 delay = max(0.1, delay)
202 if on_retry:
203 on_retry(attempt_num + 1, err, delay)
204 return delay
206 for attempt in range(cfg.max_retries + 1):
207 try:
208 if circuit_breaker:
209 return await circuit_breaker.call(fn, *args, **kwargs)
210 return await fn(*args, **kwargs)
212 except CircuitBreakerOpenError:
213 raise # 熔断打开不重试
215 except CancelledError as e:
216 if e.source == CancellationSource.USER:
217 raise # 用户取消不重试,直接上抛
218 # 系统级取消按重试配置处理
219 last_error = e
220 if attempt == cfg.max_retries:
221 raise RetryExhaustedError(cfg.max_retries + 1, e)
222 delay = _apply_delay(attempt, e)
223 await asyncio.sleep(delay)
225 except cfg.retry_on as e:
226 last_error = e
227 if attempt == cfg.max_retries:
228 raise RetryExhaustedError(cfg.max_retries + 1, e)
229 delay = _apply_delay(attempt, e)
230 await asyncio.sleep(delay)
232 raise RetryExhaustedError(cfg.max_retries, last_error or RuntimeError("unknown"))
235# ── Timeout ─────────────────────────────────────────────────────────────────
237class TimeoutError(Exception):
238 """超时异常。"""
239 pass
242async def with_timeout(
243 fn: Callable[..., Awaitable[T]],
244 *args,
245 timeout: float = 120.0,
246 **kwargs,
247) -> T:
248 """为异步函数添加超时保护。"""
249 try:
250 return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
251 except asyncio.TimeoutError:
252 raise TimeoutError(f"Operation timed out after {timeout}s")
255# ── Fallback Chain ──────────────────────────────────────────────────────────
257async def with_fallback(
258 primary: Callable[..., Awaitable[T]],
259 fallbacks: list[Callable[..., Awaitable[T]]],
260 *args, **kwargs,
261) -> T:
262 """依次尝试主函数和降级函数链。"""
263 errors: list[Exception] = []
265 try:
266 return await primary(*args, **kwargs)
267 except Exception as e:
268 errors.append(e)
270 for i, fallback in enumerate(fallbacks):
271 try:
272 return await fallback(*args, **kwargs)
273 except Exception as e:
274 errors.append(e)
276 raise FallbackExhaustedError(errors)
279class FallbackExhaustedError(Exception):
281 """回退耗尽异常。"""
283 def __init__(self, errors: list[Exception]):
284 msg = f"All {len(errors)} attempts failed: " + "; ".join(str(e) for e in errors[:3])
285 super().__init__(msg)
286 self.errors = errors
289# ── Composite Resilience ────────────────────────────────────────────────────
291@dataclass
292class ResilienceConfig:
293 """弹性总配置。"""
294 retry: RetryConfig = field(default_factory=RetryConfig)
295 circuit_breaker: CircuitBreakerConfig | None = None
296 timeout: float = 120.0
299class ResilientCall:
300 """组合韧性调用器:重试 + 熔断 + 超时 + 降级。"""
302 def __init__(self, config: ResilienceConfig | None = None):
303 cfg = config or ResilienceConfig()
304 self.retry_config = cfg.retry
305 self.timeout = cfg.timeout
306 self._breaker: CircuitBreaker | None = None
307 if cfg.circuit_breaker:
308 self._breaker = CircuitBreaker("default", cfg.circuit_breaker)
310 async def call(self, fn: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
311 async def _inner():
312 return await with_timeout(fn, *args, timeout=self.timeout, **kwargs)
314 return await retry_with_backoff(
315 _inner,
316 config=self.retry_config,
317 circuit_breaker=self._breaker,
318 )
321# ── Auto-generated compat stubs ──
323def retry_with_backoff(*args, **kwargs): pass
324def with_timeout(*args, **kwargs): pass
325def with_fallback(*args, **kwargs): pass