Files
Aether/_deprecated_py_src/utils/timeout.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

224 lines
6.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.
"""
超时保护工具
为异步函数和操作提供超时保护
"""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar
from src.core.logger import logger
T = TypeVar("T")
class AsyncTimeoutError(TimeoutError):
"""异步操作超时错误"""
def __init__(self, message: str, operation: str, timeout: float):
super().__init__(message)
self.operation = operation
self.timeout = timeout
def with_timeout(seconds: float, operation_name: str | None = None) -> Any:
"""
装饰器:为异步函数添加超时保护
Args:
seconds: 超时时间(秒)
operation_name: 操作名称(用于日志,默认使用函数名)
Usage:
@with_timeout(30.0)
async def my_async_function():
...
@with_timeout(60.0, operation_name="API请求")
async def api_call():
...
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
op_name = operation_name or func.__name__
try:
return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
except TimeoutError:
logger.warning(f"操作超时: {op_name} (timeout={seconds}s)")
raise AsyncTimeoutError(
f"{op_name} 操作超时({seconds}秒)",
operation=op_name,
timeout=seconds,
)
return wrapper
return decorator
async def run_with_timeout(
coro: Any,
timeout: float,
operation_name: str = "operation",
default: T | None = None,
raise_on_timeout: bool = True,
) -> T:
"""
为协程添加超时保护(函数式调用)
Args:
coro: 协程对象
timeout: 超时时间(秒)
operation_name: 操作名称(用于日志)
default: 超时时返回的默认值(仅在 raise_on_timeout=False 时有效)
raise_on_timeout: 超时时是否抛出异常
Returns:
协程的返回值,或超时时的默认值
Usage:
result = await run_with_timeout(
my_async_function(),
timeout=30.0,
operation_name="API请求"
)
"""
try:
return await asyncio.wait_for(coro, timeout=timeout)
except TimeoutError:
logger.warning(f"操作超时: {operation_name} (timeout={timeout}s)")
if raise_on_timeout:
raise AsyncTimeoutError(
f"{operation_name} 操作超时({timeout}秒)",
operation=operation_name,
timeout=timeout,
)
return default
class TimeoutContext:
"""
超时上下文管理器
Usage:
async with TimeoutContext(30.0, "数据库查询") as ctx:
result = await db.query(...)
# 如果超过30秒会抛出 AsyncTimeoutError
"""
def __init__(self, timeout: float, operation_name: str = "operation"):
self.timeout = timeout
self.operation_name = operation_name
self._task: asyncio.Task | None = None
async def __aenter__(self) -> None:
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
# asyncio.timeout 在 Python 3.11+ 可用
# 这里使用更通用的方式
pass
async def with_timeout_context(timeout: float, operation_name: str = "operation") -> Any:
"""
超时上下文管理器Python 3.11+ asyncio.timeout 的替代)
Usage:
async with with_timeout_context(30.0, "API请求"):
result = await api_call()
"""
try:
# Python 3.11+ 使用内置的 asyncio.timeout
return asyncio.timeout(timeout)
except AttributeError:
# Python 3.10 及以下版本的兼容实现
# 注意:这个简单实现不支持嵌套取消
pass
async def read_first_chunk_with_ttfb_timeout(
byte_iterator: Any,
timeout: float,
request_id: str,
provider_name: str,
) -> tuple[bytes, Any]:
"""
读取流的首字节并应用 TTFB 超时检测
首字节超时Time To First Byte用于检测慢响应的 Provider
超时时触发故障转移到其他可用的 Provider。
Args:
byte_iterator: 异步字节流迭代器
timeout: TTFB 超时时间(秒)
request_id: 请求 ID用于日志
provider_name: Provider 名称(用于日志和异常)
Returns:
(first_chunk, aiter): 首个字节块和异步迭代器
Raises:
ProviderTimeoutException: 如果首字节超时
"""
from src.core.exceptions import ProviderTimeoutException
aiter = byte_iterator.__aiter__()
try:
first_chunk = await asyncio.wait_for(aiter.__anext__(), timeout=timeout)
return first_chunk, aiter
except TimeoutError:
# 完整的资源清理:先关闭迭代器,再关闭底层响应
await _cleanup_iterator_resources(aiter, request_id)
logger.warning(
f" [{request_id}] 流首字节超时 (TTFB): "
f"Provider={provider_name}, timeout={timeout}s"
)
raise ProviderTimeoutException(
provider_name=provider_name,
timeout=int(timeout),
)
async def _cleanup_iterator_resources(aiter: Any, request_id: str) -> None:
"""
清理异步迭代器及其底层资源
确保在 TTFB 超时后正确释放 HTTP 连接,避免连接泄漏。
Args:
aiter: 异步迭代器
request_id: 请求 ID用于日志
"""
# 1. 关闭迭代器本身
if hasattr(aiter, "aclose"):
try:
await aiter.aclose()
except Exception as e:
logger.debug(f" [{request_id}] 关闭迭代器失败: {e}")
# 2. 关闭底层响应对象httpx.Response
# 迭代器可能持有 _response 属性指向底层响应
response = getattr(aiter, "_response", None)
if response is not None and hasattr(response, "aclose"):
try:
await response.aclose()
except Exception as e:
logger.debug(f" [{request_id}] 关闭底层响应失败: {e}")
# 3. 尝试关闭 httpx 流(如果迭代器是 httpx 的 aiter_bytes
# httpx 的 Response.aiter_bytes() 返回的生成器可能有 _stream 属性
stream = getattr(aiter, "_stream", None)
if stream is not None and hasattr(stream, "aclose"):
try:
await stream.aclose()
except Exception as e:
logger.debug(f" [{request_id}] 关闭流对象失败: {e}")