|
1 | | -"""共享 HTTP 客户端 — 复用连接池,避免每个模块各自创建""" |
2 | | -from contextlib import contextmanager |
| 1 | +"""共享 HTTP 客户端和调用级超时控制。""" |
| 2 | +import threading |
| 3 | +from collections.abc import Iterator |
| 4 | +from contextlib import asynccontextmanager, contextmanager |
3 | 5 |
|
4 | 6 | import httpx |
5 | 7 |
|
6 | 8 | _client: httpx.Client | None = None |
| 9 | +_async_client: httpx.AsyncClient | None = None |
| 10 | +_client_lock = threading.Lock() |
7 | 11 |
|
8 | 12 |
|
9 | | -@contextmanager |
10 | | -def shared_client(timeout: float = 60.0): |
11 | | - """获取共享的 httpx.Client(懒加载,单例,contextmanager 兼容 with 语句)""" |
| 13 | +class _TimeoutBoundClient: |
| 14 | + """Apply a timeout to one call without mutating the shared connection pool.""" |
| 15 | + |
| 16 | + def __init__(self, client: httpx.Client, timeout: float): |
| 17 | + self._client = client |
| 18 | + self._timeout = timeout |
| 19 | + |
| 20 | + def post(self, url: str, **kwargs) -> httpx.Response: |
| 21 | + kwargs.setdefault("timeout", self._timeout) |
| 22 | + return self._client.post(url, **kwargs) |
| 23 | + |
| 24 | + def stream(self, method: str, url: str, **kwargs): |
| 25 | + kwargs.setdefault("timeout", self._timeout) |
| 26 | + return self._client.stream(method, url, **kwargs) |
| 27 | + |
| 28 | + |
| 29 | +class _AsyncTimeoutBoundClient: |
| 30 | + """Async counterpart that shares one connection pool across chat streams.""" |
| 31 | + |
| 32 | + def __init__(self, client: httpx.AsyncClient, timeout: float): |
| 33 | + self._client = client |
| 34 | + self._timeout = timeout |
| 35 | + |
| 36 | + async def post(self, url: str, **kwargs) -> httpx.Response: |
| 37 | + kwargs.setdefault("timeout", self._timeout) |
| 38 | + return await self._client.post(url, **kwargs) |
| 39 | + |
| 40 | + def stream(self, method: str, url: str, **kwargs): |
| 41 | + kwargs.setdefault("timeout", self._timeout) |
| 42 | + return self._client.stream(method, url, **kwargs) |
| 43 | + |
| 44 | + |
| 45 | +def _get_client() -> httpx.Client: |
12 | 46 | global _client |
13 | | - if _client is None: |
14 | | - _client = httpx.Client(timeout=timeout) |
15 | | - yield _client |
| 47 | + with _client_lock: |
| 48 | + if _client is None or _client.is_closed: |
| 49 | + _client = httpx.Client( |
| 50 | + limits=httpx.Limits(max_connections=12, max_keepalive_connections=6), |
| 51 | + ) |
| 52 | + return _client |
| 53 | + |
| 54 | + |
| 55 | +def _get_async_client() -> httpx.AsyncClient: |
| 56 | + global _async_client |
| 57 | + if _async_client is None or _async_client.is_closed: |
| 58 | + _async_client = httpx.AsyncClient( |
| 59 | + limits=httpx.Limits(max_connections=12, max_keepalive_connections=6), |
| 60 | + ) |
| 61 | + return _async_client |
| 62 | + |
| 63 | + |
| 64 | +def close_shared_client() -> None: |
| 65 | + """Close the connection pool during application shutdown.""" |
| 66 | + global _client |
| 67 | + with _client_lock: |
| 68 | + client, _client = _client, None |
| 69 | + if client is not None and not client.is_closed: |
| 70 | + client.close() |
| 71 | + |
| 72 | + |
| 73 | +async def close_shared_clients() -> None: |
| 74 | + """Close both pools during application shutdown.""" |
| 75 | + global _async_client |
| 76 | + close_shared_client() |
| 77 | + async_client, _async_client = _async_client, None |
| 78 | + if async_client is not None and not async_client.is_closed: |
| 79 | + await async_client.aclose() |
| 80 | + |
| 81 | + |
| 82 | +@contextmanager |
| 83 | +def shared_client(timeout: float = 60.0) -> Iterator[_TimeoutBoundClient]: |
| 84 | + """Yield a shared pool with a timeout scoped to this individual request.""" |
| 85 | + if timeout <= 0: |
| 86 | + raise ValueError("timeout must be positive") |
| 87 | + yield _TimeoutBoundClient(_get_client(), timeout) |
| 88 | + |
| 89 | + |
| 90 | +@asynccontextmanager |
| 91 | +async def shared_async_client(timeout: float = 60.0): |
| 92 | + """Yield the async connection pool with a timeout scoped to this request.""" |
| 93 | + if timeout <= 0: |
| 94 | + raise ValueError("timeout must be positive") |
| 95 | + yield _AsyncTimeoutBoundClient(_get_async_client(), timeout) |
0 commit comments