fix(http): 支持默认HTTP客户端原子重建以恢复HTTP/2流容量

feat(db): 为外键列补充缺失的数据库索引并添加迁移脚本

- HTTPClientPool._reset_default_client 原子替换共享客户端,旧客户端在宽限期后异步关闭
- reset_upstream_client 对无代理路由不再直接跳过,改为调用 _reset_default_client
- 为 Usage, WalletTransaction, PaymentCallback, RefundRequest, VideoTask, RequestCandidate 等表的外键列添加 index=True
This commit is contained in:
fawney19
2026-03-08 01:27:50 +08:00
parent 90663793a2
commit d0eca509d4
3 changed files with 184 additions and 25 deletions

View File

@@ -54,6 +54,8 @@ class HTTPClientPool:
_max_proxy_clients: int = 50
# Tunnel 客户端缓存:{node_id: client}
_tunnel_clients: dict[str, httpx.AsyncClient] = {}
# 后台清理任务引用集合(防止被 GC 回收)
_background_tasks: set[asyncio.Task[None]] = set()
def __new__(cls) -> "HTTPClientPool":
if cls._instance is None:
@@ -457,6 +459,51 @@ class HTTPClientPool:
client_config.update(kwargs)
return httpx.AsyncClient(**client_config) # type: ignore[arg-type]
@classmethod
async def _reset_default_client(cls) -> bool:
"""Atomically replace the shared default client with a fresh instance.
The old client is kept open briefly so that in-flight requests can
finish on their existing HTTP/2 streams; it is closed asynchronously
after a short grace period.
"""
async with _default_client_lock:
old_client = cls._default_client
if old_client is None:
return False
# Create a new client before discarding the old one
cls._default_client = httpx.AsyncClient(
http2=config.enable_http2,
verify=get_ssl_context(),
timeout=httpx.Timeout(
connect=config.http_connect_timeout,
read=config.http_read_timeout,
write=config.http_write_timeout,
pool=config.http_pool_timeout,
),
limits=httpx.Limits(
max_connections=config.http_max_connections,
max_keepalive_connections=config.http_keepalive_connections,
keepalive_expiry=config.http_keepalive_expiry,
),
follow_redirects=True,
)
# Close old client after a grace period so in-flight requests can drain
async def _close_old() -> None:
await asyncio.sleep(5)
try:
await old_client.aclose()
except Exception as exc:
logger.warning("关闭旧默认客户端失败: {}", exc)
task = asyncio.create_task(_close_old())
cls._background_tasks.add(task)
task.add_done_callback(cls._background_tasks.discard)
logger.warning("默认HTTP客户端已重建(HTTP/2 流容量恢复)")
return True
@classmethod
async def reset_upstream_client(
cls,
@@ -467,8 +514,9 @@ class HTTPClientPool:
"""Reset cached upstream client for the given proxy/tunnel route.
Returns True when a cached client was closed and removed.
For the shared no-proxy default client this is a no-op to avoid
disrupting unrelated in-flight requests.
For the shared no-proxy default client, atomically replaces it with a
new instance so that subsequent requests get a fresh HTTP/2 connection
while in-flight requests on the old client can finish naturally.
"""
if delegate_cfg and delegate_cfg.get("tunnel"):
node_id = str(delegate_cfg.get("node_id") or "")
@@ -490,7 +538,7 @@ class HTTPClientPool:
base_cache_key = compute_proxy_cache_key(proxy_config)
if base_cache_key == "__no_proxy__":
return False
return await cls._reset_default_client()
cache_key_prefixes = [base_cache_key]
tls_profile_key = str(tls_profile or "").strip().lower()