mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(proxy,failover,transport): Hyper 上游客户端精细计时、连续失败退避与连接泄漏修复
Proxy: - 将上游 HTTP 客户端从 reqwest 替换为 hyper,新增 InstrumentedConnector 实现 TCP 连接/TLS 握手级别的独立计时,上报 connection_reused 等指标 - 前端展示细粒度代理计时(连接复用、等待响应头等) Failover: - 引入连续失败退避机制,每 10 次失败递增退避间隔 - 检测 H2 max outbound streams 错误并触发上游客户端重建 - 新增 HTTPClientPool.reset_upstream_client 支持按需重建缓存客户端 连接泄漏修复: - Handler 异常路径确保 response_ctx 被正确关闭 - HubResponseStream 迭代结束后在 finally 块中清理 stream_id - HubTunnelTransport.handle_request 捕获所有异常并清理流状态
This commit is contained in:
@@ -458,6 +458,67 @@ class HTTPClientPool:
|
||||
return httpx.AsyncClient(**client_config) # type: ignore[arg-type]
|
||||
|
||||
@classmethod
|
||||
async def reset_upstream_client(
|
||||
cls,
|
||||
delegate_cfg: dict[str, Any] | None,
|
||||
proxy_config: dict[str, Any] | None = None,
|
||||
tls_profile: str | None = None,
|
||||
) -> bool:
|
||||
"""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.
|
||||
"""
|
||||
if delegate_cfg and delegate_cfg.get("tunnel"):
|
||||
node_id = str(delegate_cfg.get("node_id") or "")
|
||||
if not node_id:
|
||||
return False
|
||||
lock = cls._get_proxy_clients_lock()
|
||||
async with lock:
|
||||
client = cls._tunnel_clients.pop(node_id, None)
|
||||
if client is None:
|
||||
return False
|
||||
try:
|
||||
await client.aclose()
|
||||
except Exception as exc:
|
||||
logger.warning("关闭 Tunnel 客户端失败(node_id={}): {}", node_id, exc)
|
||||
return True
|
||||
|
||||
if not proxy_config:
|
||||
proxy_config = get_system_proxy_config()
|
||||
|
||||
base_cache_key = compute_proxy_cache_key(proxy_config)
|
||||
if base_cache_key == "__no_proxy__":
|
||||
return False
|
||||
|
||||
cache_key_prefixes = [base_cache_key]
|
||||
tls_profile_key = str(tls_profile or "").strip().lower()
|
||||
if tls_profile_key:
|
||||
cache_key_prefixes = [f"{base_cache_key}::tls:{tls_profile_key}"]
|
||||
|
||||
lock = cls._get_proxy_clients_lock()
|
||||
async with lock:
|
||||
keys_to_remove = [
|
||||
key
|
||||
for key in list(cls._proxy_clients.keys())
|
||||
if any(
|
||||
key == prefix
|
||||
or key.startswith(f"{prefix}::")
|
||||
or key.startswith(f"{prefix}::tls:")
|
||||
for prefix in cache_key_prefixes
|
||||
)
|
||||
]
|
||||
clients = [cls._proxy_clients.pop(key)[0] for key in keys_to_remove]
|
||||
|
||||
for client in clients:
|
||||
try:
|
||||
await client.aclose()
|
||||
except Exception as exc:
|
||||
logger.warning("关闭上游代理客户端失败: {}", exc)
|
||||
|
||||
return bool(clients)
|
||||
|
||||
async def get_upstream_client(
|
||||
cls,
|
||||
delegate_cfg: dict[str, Any] | None,
|
||||
|
||||
Reference in New Issue
Block a user