mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(proxy): 将 proxy resolver 同步阻塞操作异步化,避免阻塞事件循环
- 为 resolve_proxy_info、resolve_delegate_config、build_proxy_url、 get_system_proxy_config、build_post_kwargs、build_stream_kwargs 新增 _async 异步版本,通过 asyncio.to_thread 在工作线程中执行同步 DB 查询 - 为 _proxy_node_cache 和 _system_proxy_cache 添加 threading.Lock 保护 多线程并发读写安全 - 大 payload 的 gzip 压缩超过 64KB 阈值时走线程池,小 payload 仍在事件 循环中同步执行以避免不必要的线程调度开销 - hub_transport 的 frame 压缩同样增加异步版本 - 删除已无调用者的同步方法 create_client_with_proxy,将其逻辑内联至 get_upstream_client 并改为异步 - 更新所有 handler/executor/failover 调用点使用新的异步 API - 补充 async 版本的单元测试
This commit is contained in:
@@ -930,11 +930,11 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
get_proxy_label,
|
get_proxy_label,
|
||||||
resolve_effective_proxy,
|
resolve_effective_proxy,
|
||||||
resolve_proxy_info,
|
resolve_proxy_info_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
||||||
ctx.proxy_info = resolve_proxy_info(effective_proxy)
|
ctx.proxy_info = await resolve_proxy_info_async(effective_proxy)
|
||||||
proxy_label = get_proxy_label(ctx.proxy_info)
|
proxy_label = get_proxy_label(ctx.proxy_info)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -946,10 +946,13 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
# simulate streaming to the client (sync -> stream bridge).
|
# simulate streaming to the client (sync -> stream bridge).
|
||||||
if not upstream_is_stream:
|
if not upstream_is_stream:
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import build_post_kwargs, resolve_delegate_config
|
from src.services.proxy_node.resolver import (
|
||||||
|
build_post_kwargs_async,
|
||||||
|
resolve_delegate_config_async,
|
||||||
|
)
|
||||||
|
|
||||||
request_timeout_sync = provider.request_timeout or config.http_request_timeout
|
request_timeout_sync = provider.request_timeout or config.http_request_timeout
|
||||||
delegate_cfg = resolve_delegate_config(effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=effective_proxy,
|
proxy_config=effective_proxy,
|
||||||
@@ -957,7 +960,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
@@ -997,7 +1000,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
ctx.provider_request_headers = provider_headers
|
ctx.provider_request_headers = provider_headers
|
||||||
|
|
||||||
# retry once
|
# retry once
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
@@ -1147,9 +1150,12 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
# 获取 HTTP 客户端(支持代理配置,Key 级别优先于 Provider 级别)
|
# 获取 HTTP 客户端(支持代理配置,Key 级别优先于 Provider 级别)
|
||||||
# 使用连接池复用客户端,避免每次流式请求都新建 TCP/TLS 连接
|
# 使用连接池复用客户端,避免每次流式请求都新建 TCP/TLS 连接
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import build_stream_kwargs, resolve_delegate_config
|
from src.services.proxy_node.resolver import (
|
||||||
|
build_stream_kwargs_async,
|
||||||
|
resolve_delegate_config_async,
|
||||||
|
)
|
||||||
|
|
||||||
delegate_cfg = resolve_delegate_config(effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=effective_proxy,
|
proxy_config=effective_proxy,
|
||||||
@@ -1164,7 +1170,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
async def _connect_and_prefetch() -> None:
|
async def _connect_and_prefetch() -> None:
|
||||||
"""建立连接并预读首字节(受整体超时控制)"""
|
"""建立连接并预读首字节(受整体超时控制)"""
|
||||||
nonlocal byte_iterator, prefetched_chunks, response_ctx
|
nonlocal byte_iterator, prefetched_chunks, response_ctx
|
||||||
_skw = build_stream_kwargs(
|
_skw = await build_stream_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
|
|||||||
@@ -517,11 +517,11 @@ class ChatSyncExecutor:
|
|||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
get_proxy_label,
|
get_proxy_label,
|
||||||
resolve_effective_proxy,
|
resolve_effective_proxy,
|
||||||
resolve_proxy_info,
|
resolve_proxy_info_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
_effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
_effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
||||||
ctx.sync_proxy_info = resolve_proxy_info(_effective_proxy)
|
ctx.sync_proxy_info = await resolve_proxy_info_async(_effective_proxy)
|
||||||
_proxy_label = get_proxy_label(ctx.sync_proxy_info)
|
_proxy_label = get_proxy_label(ctx.sync_proxy_info)
|
||||||
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
provider_type = str(getattr(provider, "provider_type", "") or "").lower()
|
||||||
|
|
||||||
@@ -538,16 +538,16 @@ class ChatSyncExecutor:
|
|||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.config.settings import config
|
from src.config.settings import config
|
||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
build_post_kwargs,
|
build_post_kwargs_async,
|
||||||
build_stream_kwargs,
|
build_stream_kwargs_async,
|
||||||
resolve_delegate_config,
|
resolve_delegate_config_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 非流式请求使用 http_request_timeout 作为整体超时
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
# 优先使用 Provider 配置,否则使用全局配置
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
request_timeout = provider.request_timeout or config.http_request_timeout
|
request_timeout = provider.request_timeout or config.http_request_timeout
|
||||||
|
|
||||||
delegate_cfg = resolve_delegate_config(_effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(_effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=_effective_proxy,
|
proxy_config=_effective_proxy,
|
||||||
@@ -559,7 +559,7 @@ class ChatSyncExecutor:
|
|||||||
resp: httpx.Response | None = None
|
resp: httpx.Response | None = None
|
||||||
if not upstream_is_stream:
|
if not upstream_is_stream:
|
||||||
try:
|
try:
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_hdrs,
|
headers=provider_hdrs,
|
||||||
@@ -584,7 +584,7 @@ class ChatSyncExecutor:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_stream_args = build_stream_kwargs(
|
_stream_args = await build_stream_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_hdrs,
|
headers=provider_hdrs,
|
||||||
|
|||||||
@@ -455,19 +455,22 @@ class CliStreamMixin:
|
|||||||
# 解析有效代理(Key 级别优先于 Provider 级别)
|
# 解析有效代理(Key 级别优先于 Provider 级别)
|
||||||
from src.services.proxy_node.resolver import get_proxy_label as _gpl
|
from src.services.proxy_node.resolver import get_proxy_label as _gpl
|
||||||
from src.services.proxy_node.resolver import resolve_effective_proxy as _rep
|
from src.services.proxy_node.resolver import resolve_effective_proxy as _rep
|
||||||
from src.services.proxy_node.resolver import resolve_proxy_info as _rpi
|
from src.services.proxy_node.resolver import resolve_proxy_info_async as _rpi_async
|
||||||
|
|
||||||
effective_proxy = _rep(provider.proxy, getattr(key, "proxy", None))
|
effective_proxy = _rep(provider.proxy, getattr(key, "proxy", None))
|
||||||
ctx.proxy_info = _rpi(effective_proxy)
|
ctx.proxy_info = await _rpi_async(effective_proxy)
|
||||||
|
|
||||||
# If upstream is forced to non-stream mode, we execute a sync request and then
|
# If upstream is forced to non-stream mode, we execute a sync request and then
|
||||||
# simulate streaming to the client (sync -> stream bridge).
|
# simulate streaming to the client (sync -> stream bridge).
|
||||||
if not upstream_is_stream:
|
if not upstream_is_stream:
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import build_post_kwargs, resolve_delegate_config
|
from src.services.proxy_node.resolver import (
|
||||||
|
build_post_kwargs_async,
|
||||||
|
resolve_delegate_config_async,
|
||||||
|
)
|
||||||
|
|
||||||
request_timeout_sync = provider.request_timeout or config.http_request_timeout
|
request_timeout_sync = provider.request_timeout or config.http_request_timeout
|
||||||
delegate_cfg = resolve_delegate_config(effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=effective_proxy,
|
proxy_config=effective_proxy,
|
||||||
@@ -475,7 +478,7 @@ class CliStreamMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
@@ -517,7 +520,7 @@ class CliStreamMixin:
|
|||||||
ctx.provider_request_headers = provider_headers
|
ctx.provider_request_headers = provider_headers
|
||||||
|
|
||||||
# retry once
|
# retry once
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
@@ -663,9 +666,12 @@ class CliStreamMixin:
|
|||||||
# 获取 HTTP 客户端(支持代理配置,Key 级别优先于 Provider 级别)
|
# 获取 HTTP 客户端(支持代理配置,Key 级别优先于 Provider 级别)
|
||||||
# 使用连接池复用客户端,避免每次流式请求都新建 TCP/TLS 连接
|
# 使用连接池复用客户端,避免每次流式请求都新建 TCP/TLS 连接
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import build_stream_kwargs, resolve_delegate_config
|
from src.services.proxy_node.resolver import (
|
||||||
|
build_stream_kwargs_async,
|
||||||
|
resolve_delegate_config_async,
|
||||||
|
)
|
||||||
|
|
||||||
delegate_cfg = resolve_delegate_config(effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=effective_proxy,
|
proxy_config=effective_proxy,
|
||||||
@@ -680,7 +686,7 @@ class CliStreamMixin:
|
|||||||
async def _connect_and_prefetch() -> None:
|
async def _connect_and_prefetch() -> None:
|
||||||
"""建立连接并预读首字节(受整体超时控制)"""
|
"""建立连接并预读首字节(受整体超时控制)"""
|
||||||
nonlocal byte_iterator, prefetched_chunks, response_ctx
|
nonlocal byte_iterator, prefetched_chunks, response_ctx
|
||||||
_skw = build_stream_kwargs(
|
_skw = await build_stream_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
|
|||||||
@@ -272,11 +272,11 @@ class CliSyncMixin:
|
|||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
get_proxy_label,
|
get_proxy_label,
|
||||||
resolve_effective_proxy,
|
resolve_effective_proxy,
|
||||||
resolve_proxy_info,
|
resolve_proxy_info_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
_effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
_effective_proxy = resolve_effective_proxy(provider.proxy, getattr(key, "proxy", None))
|
||||||
sync_proxy_info = resolve_proxy_info(_effective_proxy)
|
sync_proxy_info = await resolve_proxy_info_async(_effective_proxy)
|
||||||
_proxy_label = get_proxy_label(sync_proxy_info)
|
_proxy_label = get_proxy_label(sync_proxy_info)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -291,16 +291,16 @@ class CliSyncMixin:
|
|||||||
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
build_post_kwargs,
|
build_post_kwargs_async,
|
||||||
build_stream_kwargs,
|
build_stream_kwargs_async,
|
||||||
resolve_delegate_config,
|
resolve_delegate_config_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 非流式请求使用 http_request_timeout 作为整体超时
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
# 优先使用 Provider 配置,否则使用全局配置
|
# 优先使用 Provider 配置,否则使用全局配置
|
||||||
request_timeout = provider.request_timeout or config.http_request_timeout
|
request_timeout = provider.request_timeout or config.http_request_timeout
|
||||||
|
|
||||||
delegate_cfg = resolve_delegate_config(_effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(_effective_proxy)
|
||||||
http_client = await HTTPClientPool.get_upstream_client(
|
http_client = await HTTPClientPool.get_upstream_client(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
proxy_config=_effective_proxy,
|
proxy_config=_effective_proxy,
|
||||||
@@ -312,7 +312,7 @@ class CliSyncMixin:
|
|||||||
resp: httpx.Response | None = None
|
resp: httpx.Response | None = None
|
||||||
if not upstream_is_stream:
|
if not upstream_is_stream:
|
||||||
try:
|
try:
|
||||||
_pkw = build_post_kwargs(
|
_pkw = await build_post_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
@@ -337,7 +337,7 @@ class CliSyncMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_stream_args = build_stream_kwargs(
|
_stream_args = await build_stream_kwargs_async(
|
||||||
delegate_cfg,
|
delegate_cfg,
|
||||||
url=url,
|
url=url,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ from src.config import config
|
|||||||
from src.core.logger import logger
|
from src.core.logger import logger
|
||||||
from src.services.provider.fingerprint import KNOWN_IMPERSONATE_PROFILES
|
from src.services.provider.fingerprint import KNOWN_IMPERSONATE_PROFILES
|
||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
build_proxy_url,
|
build_proxy_url_async,
|
||||||
compute_proxy_cache_key,
|
compute_proxy_cache_key,
|
||||||
get_system_proxy_config,
|
get_system_proxy_config_async,
|
||||||
make_proxy_param,
|
make_proxy_param,
|
||||||
|
resolve_delegate_config_async,
|
||||||
)
|
)
|
||||||
from src.utils.ssl_utils import get_ssl_context, get_ssl_context_for_profile
|
from src.utils.ssl_utils import get_ssl_context, get_ssl_context_for_profile
|
||||||
|
|
||||||
@@ -222,12 +223,9 @@ class HTTPClientPool:
|
|||||||
"""
|
"""
|
||||||
# 无特定代理时,回退到系统默认代理
|
# 无特定代理时,回退到系统默认代理
|
||||||
if not proxy_config:
|
if not proxy_config:
|
||||||
proxy_config = get_system_proxy_config()
|
proxy_config = await get_system_proxy_config_async()
|
||||||
|
|
||||||
# tunnel 模式检查:tunnel 节点走专用的 TunnelTransport 客户端
|
delegate_cfg = await resolve_delegate_config_async(proxy_config)
|
||||||
from src.services.proxy_node.resolver import resolve_delegate_config
|
|
||||||
|
|
||||||
delegate_cfg = resolve_delegate_config(proxy_config)
|
|
||||||
if delegate_cfg and delegate_cfg.get("tunnel"):
|
if delegate_cfg and delegate_cfg.get("tunnel"):
|
||||||
return await cls._get_tunnel_client(delegate_cfg["node_id"])
|
return await cls._get_tunnel_client(delegate_cfg["node_id"])
|
||||||
|
|
||||||
@@ -262,7 +260,7 @@ class HTTPClientPool:
|
|||||||
await cls._evict_lru_proxy_client()
|
await cls._evict_lru_proxy_client()
|
||||||
|
|
||||||
# 添加代理配置
|
# 添加代理配置
|
||||||
proxy_url = build_proxy_url(proxy_config) if proxy_config else None
|
proxy_url = await build_proxy_url_async(proxy_config) if proxy_config else None
|
||||||
|
|
||||||
# curl_cffi Transport: real TLS fingerprint impersonation.
|
# curl_cffi Transport: real TLS fingerprint impersonation.
|
||||||
# Supports:
|
# Supports:
|
||||||
@@ -424,57 +422,6 @@ class HTTPClientPool:
|
|||||||
finally:
|
finally:
|
||||||
await client.aclose()
|
await client.aclose()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_client_with_proxy(
|
|
||||||
cls,
|
|
||||||
proxy_config: dict[str, Any] | None = None,
|
|
||||||
timeout: httpx.Timeout | None = None,
|
|
||||||
tls_profile: str | None = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> httpx.AsyncClient:
|
|
||||||
"""
|
|
||||||
创建带代理配置的HTTP客户端
|
|
||||||
|
|
||||||
⚠️ 性能警告:此方法每次都创建新客户端,推荐使用 get_proxy_client() 复用连接。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
proxy_config: 代理配置字典,包含 url, username, password
|
|
||||||
timeout: 超时配置
|
|
||||||
**kwargs: 其他 httpx.AsyncClient 配置参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
配置好的 httpx.AsyncClient 实例(调用者需要负责关闭)
|
|
||||||
"""
|
|
||||||
client_config: dict[str, Any] = {
|
|
||||||
"http2": config.enable_http2,
|
|
||||||
"verify": get_ssl_context_for_profile(tls_profile),
|
|
||||||
"follow_redirects": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeout:
|
|
||||||
client_config["timeout"] = timeout
|
|
||||||
else:
|
|
||||||
client_config["timeout"] = httpx.Timeout(
|
|
||||||
connect=config.http_connect_timeout,
|
|
||||||
read=config.http_read_timeout,
|
|
||||||
write=config.http_write_timeout,
|
|
||||||
pool=config.http_pool_timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 无特定代理时,回退到系统默认代理(与 get_proxy_client 行为一致)
|
|
||||||
if proxy_config is None:
|
|
||||||
proxy_config = get_system_proxy_config()
|
|
||||||
|
|
||||||
# 添加代理配置
|
|
||||||
proxy_url = build_proxy_url(proxy_config) if proxy_config else None
|
|
||||||
proxy_param = make_proxy_param(proxy_url)
|
|
||||||
if proxy_param:
|
|
||||||
client_config["proxy"] = proxy_param
|
|
||||||
logger.debug("创建带代理的HTTP客户端(一次性): {}", proxy_config.get("url", "unknown"))
|
|
||||||
|
|
||||||
client_config.update(kwargs)
|
|
||||||
return httpx.AsyncClient(**client_config) # type: ignore[arg-type]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _reset_default_client(cls) -> bool:
|
async def _reset_default_client(cls) -> bool:
|
||||||
"""Atomically replace the shared default client with a fresh instance.
|
"""Atomically replace the shared default client with a fresh instance.
|
||||||
@@ -551,7 +498,7 @@ class HTTPClientPool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if not proxy_config:
|
if not proxy_config:
|
||||||
proxy_config = get_system_proxy_config()
|
proxy_config = await get_system_proxy_config_async()
|
||||||
|
|
||||||
base_cache_key = compute_proxy_cache_key(proxy_config)
|
base_cache_key = compute_proxy_cache_key(proxy_config)
|
||||||
if base_cache_key == "__no_proxy__":
|
if base_cache_key == "__no_proxy__":
|
||||||
@@ -616,11 +563,37 @@ class HTTPClientPool:
|
|||||||
"""
|
"""
|
||||||
if delegate_cfg and delegate_cfg.get("tunnel"):
|
if delegate_cfg and delegate_cfg.get("tunnel"):
|
||||||
return await cls._get_tunnel_client(delegate_cfg["node_id"], timeout=timeout)
|
return await cls._get_tunnel_client(delegate_cfg["node_id"], timeout=timeout)
|
||||||
return cls.create_client_with_proxy(
|
client_config: dict[str, Any] = {
|
||||||
proxy_config=proxy_config,
|
"http2": config.enable_http2,
|
||||||
timeout=timeout,
|
"verify": get_ssl_context_for_profile(tls_profile),
|
||||||
tls_profile=tls_profile,
|
"follow_redirects": True,
|
||||||
|
}
|
||||||
|
if timeout:
|
||||||
|
client_config["timeout"] = timeout
|
||||||
|
else:
|
||||||
|
client_config["timeout"] = httpx.Timeout(
|
||||||
|
connect=config.http_connect_timeout,
|
||||||
|
read=config.http_read_timeout,
|
||||||
|
write=config.http_write_timeout,
|
||||||
|
pool=config.http_pool_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_proxy_config = proxy_config
|
||||||
|
if resolved_proxy_config is None:
|
||||||
|
resolved_proxy_config = await get_system_proxy_config_async()
|
||||||
|
|
||||||
|
proxy_url = (
|
||||||
|
await build_proxy_url_async(resolved_proxy_config) if resolved_proxy_config else None
|
||||||
)
|
)
|
||||||
|
proxy_param = make_proxy_param(proxy_url)
|
||||||
|
if proxy_param:
|
||||||
|
client_config["proxy"] = proxy_param
|
||||||
|
logger.debug(
|
||||||
|
"创建带代理的HTTP客户端(一次性): {}",
|
||||||
|
resolved_proxy_config.get("url", "unknown") if resolved_proxy_config else "unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
return httpx.AsyncClient(**client_config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_tunnel_client(
|
async def _get_tunnel_client(
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class FailoverEngine:
|
|||||||
async def _rotate_upstream_client(self, candidate: ProviderCandidate) -> bool:
|
async def _rotate_upstream_client(self, candidate: ProviderCandidate) -> bool:
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
resolve_delegate_config,
|
resolve_delegate_config_async,
|
||||||
resolve_effective_proxy,
|
resolve_effective_proxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class FailoverEngine:
|
|||||||
getattr(candidate.provider, "proxy", None),
|
getattr(candidate.provider, "proxy", None),
|
||||||
getattr(candidate.key, "proxy", None),
|
getattr(candidate.key, "proxy", None),
|
||||||
)
|
)
|
||||||
delegate_cfg = resolve_delegate_config(effective_proxy)
|
delegate_cfg = await resolve_delegate_config_async(effective_proxy)
|
||||||
return await HTTPClientPool.reset_upstream_client(
|
return await HTTPClientPool.reset_upstream_client(
|
||||||
delegate_cfg, proxy_config=effective_proxy
|
delegate_cfg, proxy_config=effective_proxy
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
_TUNNEL_COMPRESS_MIN_SIZE = 512
|
_TUNNEL_COMPRESS_MIN_SIZE = 512
|
||||||
|
_TUNNEL_ASYNC_COMPRESS_THRESHOLD = 64 * 1024
|
||||||
_RECONNECT_DELAYS_SECONDS: tuple[float, ...] = (0.0, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0)
|
_RECONNECT_DELAYS_SECONDS: tuple[float, ...] = (0.0, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0)
|
||||||
_HEARTBEAT_DEDUP_TTL_SECONDS = 600
|
_HEARTBEAT_DEDUP_TTL_SECONDS = 600
|
||||||
_LOOP_WATCHDOG_INTERVAL_SECONDS = 1.0
|
_LOOP_WATCHDOG_INTERVAL_SECONDS = 1.0
|
||||||
@@ -619,7 +620,7 @@ class HubConnectionManager:
|
|||||||
|
|
||||||
body_data = body or b""
|
body_data = body or b""
|
||||||
if body_data:
|
if body_data:
|
||||||
body_payload, body_flags = _compress_frame_payload(body_data)
|
body_payload, body_flags = await _compress_frame_payload_async(body_data)
|
||||||
else:
|
else:
|
||||||
body_payload, body_flags = body_data, 0
|
body_payload, body_flags = body_data, 0
|
||||||
body_flags |= FrameFlags.END_STREAM
|
body_flags |= FrameFlags.END_STREAM
|
||||||
@@ -783,6 +784,12 @@ def _compress_frame_payload(data: bytes) -> tuple[bytes, int]:
|
|||||||
return data, 0
|
return data, 0
|
||||||
|
|
||||||
|
|
||||||
|
async def _compress_frame_payload_async(data: bytes) -> tuple[bytes, int]:
|
||||||
|
if len(data) < _TUNNEL_ASYNC_COMPRESS_THRESHOLD:
|
||||||
|
return _compress_frame_payload(data)
|
||||||
|
return await asyncio.to_thread(_compress_frame_payload, data)
|
||||||
|
|
||||||
|
|
||||||
def _decompress_frame_payload(frame: Frame) -> bytes:
|
def _decompress_frame_payload(frame: Frame) -> bytes:
|
||||||
if frame.is_gzip:
|
if frame.is_gzip:
|
||||||
return gzip.decompress(frame.payload)
|
return gzip.decompress(frame.payload)
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import quote, urlparse
|
from urllib.parse import quote, urlparse
|
||||||
@@ -24,18 +26,22 @@ from src.core.logger import logger
|
|||||||
# ProxyNode 信息缓存(降低高频 DB 查询开销)
|
# ProxyNode 信息缓存(降低高频 DB 查询开销)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_proxy_node_cache: dict[str, tuple[dict[str, Any] | None, float]] = {}
|
_proxy_node_cache: dict[str, tuple[dict[str, Any] | None, float]] = {}
|
||||||
|
_proxy_node_cache_lock = threading.Lock()
|
||||||
_PROXY_NODE_CACHE_TTL_SECONDS = 15.0
|
_PROXY_NODE_CACHE_TTL_SECONDS = 15.0
|
||||||
_PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS = 5.0 # 不可用节点使用更短的 TTL,加速恢复感知
|
_PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS = 5.0 # 不可用节点使用更短的 TTL,加速恢复感知
|
||||||
_PROXY_NODE_CACHE_MAX_SIZE = 256
|
_PROXY_NODE_CACHE_MAX_SIZE = 256
|
||||||
|
|
||||||
|
# payload 超过此阈值时 build_*_kwargs_async 才走 to_thread,
|
||||||
|
# 避免小 payload 承担不必要的线程调度开销
|
||||||
|
_ASYNC_PAYLOAD_THRESHOLD = 64 * 1024
|
||||||
|
|
||||||
|
|
||||||
def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
读取 ProxyNode 信息(带内存 TTL 缓存)
|
读取 ProxyNode 信息(带内存 TTL 缓存)
|
||||||
|
|
||||||
NOTE: 使用同步 DB session(create_session),在 async 上下文中会短暂阻塞
|
通过 asyncio.to_thread 在工作线程中运行,使用 _proxy_node_cache_lock 保护
|
||||||
事件循环。60s TTL 缓存覆盖绝大多数请求,阻塞仅发生在缓存未命中时。
|
缓存的并发读写安全。
|
||||||
若后续 delegate 模式导致调用频率显著上升,应考虑改为 run_in_executor 包装。
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
aether-proxy 节点: {"ip": str, "port": int, "name": str, ...}
|
aether-proxy 节点: {"ip": str, "port": int, "name": str, ...}
|
||||||
@@ -43,20 +49,16 @@ def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
|||||||
不存在/非在线: None
|
不存在/非在线: None
|
||||||
"""
|
"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
cached = _proxy_node_cache.get(node_id)
|
|
||||||
if cached:
|
|
||||||
value, expires_at = cached
|
|
||||||
if now < expires_at:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# 防止无效 node_id 导致缓存无限膨胀:淘汰最旧的条目而非全部清除
|
# 快速路径:缓存命中
|
||||||
if len(_proxy_node_cache) >= _PROXY_NODE_CACHE_MAX_SIZE:
|
with _proxy_node_cache_lock:
|
||||||
# 按过期时间排序,删除最旧的 25%
|
cached = _proxy_node_cache.get(node_id)
|
||||||
evict_count = _PROXY_NODE_CACHE_MAX_SIZE // 4
|
if cached:
|
||||||
sorted_keys = sorted(_proxy_node_cache, key=lambda k: _proxy_node_cache[k][1])
|
value, expires_at = cached
|
||||||
for k in sorted_keys[:evict_count]:
|
if now < expires_at:
|
||||||
del _proxy_node_cache[k]
|
return value
|
||||||
|
|
||||||
|
# 缓存未命中,查询 DB
|
||||||
from src.database import create_session
|
from src.database import create_session
|
||||||
from src.models.database import ProxyNode, ProxyNodeStatus
|
from src.models.database import ProxyNode, ProxyNodeStatus
|
||||||
|
|
||||||
@@ -64,71 +66,77 @@ def _get_proxy_node_info(node_id: str) -> dict[str, Any] | None:
|
|||||||
try:
|
try:
|
||||||
node = db.query(ProxyNode).filter(ProxyNode.id == node_id).first()
|
node = db.query(ProxyNode).filter(ProxyNode.id == node_id).first()
|
||||||
if not node:
|
if not node:
|
||||||
_proxy_node_cache[node_id] = (None, now + _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS)
|
result: dict[str, Any] | None = None
|
||||||
return None
|
ttl = _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS
|
||||||
|
elif node.tunnel_mode and not node.is_manual:
|
||||||
# tunnel 模式节点:统一以 DB 的 tunnel_connected/status 为准(由 Hub 广播维护)
|
|
||||||
if node.tunnel_mode and not node.is_manual:
|
|
||||||
if node.status != ProxyNodeStatus.ONLINE or not bool(node.tunnel_connected):
|
if node.status != ProxyNodeStatus.ONLINE or not bool(node.tunnel_connected):
|
||||||
_proxy_node_cache[node_id] = (
|
result = None
|
||||||
None,
|
ttl = _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS
|
||||||
now + _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS,
|
else:
|
||||||
)
|
result = {
|
||||||
return None
|
"name": node.name,
|
||||||
value: dict[str, Any] = {
|
"ip": node.ip,
|
||||||
"name": node.name,
|
"port": node.port,
|
||||||
"ip": node.ip,
|
"tunnel_mode": True,
|
||||||
"port": node.port,
|
"tunnel_connected": True,
|
||||||
"tunnel_mode": True,
|
}
|
||||||
"tunnel_connected": True,
|
ttl = _PROXY_NODE_CACHE_TTL_SECONDS
|
||||||
}
|
elif node.status != ProxyNodeStatus.ONLINE:
|
||||||
_proxy_node_cache[node_id] = (value, now + _PROXY_NODE_CACHE_TTL_SECONDS)
|
result = None
|
||||||
return value
|
ttl = _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS
|
||||||
|
elif node.is_manual:
|
||||||
# 手动节点 / 非 tunnel 节点:仍依赖 DB status
|
result = {
|
||||||
if node.status != ProxyNodeStatus.ONLINE:
|
|
||||||
_proxy_node_cache[node_id] = (None, now + _PROXY_NODE_CACHE_NEGATIVE_TTL_SECONDS)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if node.is_manual:
|
|
||||||
value = {
|
|
||||||
"is_manual": True,
|
"is_manual": True,
|
||||||
"name": node.name,
|
"name": node.name,
|
||||||
"proxy_url": node.proxy_url,
|
"proxy_url": node.proxy_url,
|
||||||
"username": node.proxy_username,
|
"username": node.proxy_username,
|
||||||
"password": node.proxy_password,
|
"password": node.proxy_password,
|
||||||
}
|
}
|
||||||
|
ttl = _PROXY_NODE_CACHE_TTL_SECONDS
|
||||||
else:
|
else:
|
||||||
value = {
|
result = {
|
||||||
"name": node.name,
|
"name": node.name,
|
||||||
"ip": node.ip,
|
"ip": node.ip,
|
||||||
"port": node.port,
|
"port": node.port,
|
||||||
"tunnel_mode": bool(node.tunnel_mode),
|
"tunnel_mode": bool(node.tunnel_mode),
|
||||||
"tunnel_connected": bool(node.tunnel_connected),
|
"tunnel_connected": bool(node.tunnel_connected),
|
||||||
}
|
}
|
||||||
|
ttl = _PROXY_NODE_CACHE_TTL_SECONDS
|
||||||
_proxy_node_cache[node_id] = (value, now + _PROXY_NODE_CACHE_TTL_SECONDS)
|
|
||||||
return value
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
# 写回缓存(持锁做淘汰+写入,使用新时间戳以排除 DB 查询耗时)
|
||||||
|
write_now = time.time()
|
||||||
|
with _proxy_node_cache_lock:
|
||||||
|
if len(_proxy_node_cache) >= _PROXY_NODE_CACHE_MAX_SIZE:
|
||||||
|
evict_count = _PROXY_NODE_CACHE_MAX_SIZE // 4
|
||||||
|
sorted_keys = sorted(_proxy_node_cache, key=lambda k: _proxy_node_cache[k][1])
|
||||||
|
for k in sorted_keys[:evict_count]:
|
||||||
|
del _proxy_node_cache[k]
|
||||||
|
_proxy_node_cache[node_id] = (result, write_now + ttl)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 系统默认代理
|
# 系统默认代理
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_system_proxy_cache: tuple[dict[str, Any] | None, float] | None = None
|
_system_proxy_cache: tuple[dict[str, Any] | None, float] | None = None
|
||||||
|
_system_proxy_cache_lock = threading.Lock()
|
||||||
_SYSTEM_PROXY_CACHE_TTL = 60.0
|
_SYSTEM_PROXY_CACHE_TTL = 60.0
|
||||||
|
|
||||||
|
|
||||||
def invalidate_proxy_node_cache(node_id: str) -> None:
|
def invalidate_proxy_node_cache(node_id: str) -> None:
|
||||||
"""主动清除指定节点的信息缓存(tunnel 断开时调用,避免使用过期的连接状态)"""
|
"""主动清除指定节点的信息缓存(tunnel 断开时调用,避免使用过期的连接状态)"""
|
||||||
_proxy_node_cache.pop(node_id, None)
|
with _proxy_node_cache_lock:
|
||||||
|
_proxy_node_cache.pop(node_id, None)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_system_proxy_cache() -> None:
|
def invalidate_system_proxy_cache() -> None:
|
||||||
"""手动失效系统代理缓存(在删除节点等操作后调用)"""
|
"""手动失效系统代理缓存(在删除节点等操作后调用)"""
|
||||||
global _system_proxy_cache
|
global _system_proxy_cache
|
||||||
_system_proxy_cache = None
|
with _system_proxy_cache_lock:
|
||||||
|
_system_proxy_cache = None
|
||||||
|
|
||||||
|
|
||||||
def get_system_proxy_config() -> dict[str, Any] | None:
|
def get_system_proxy_config() -> dict[str, Any] | None:
|
||||||
@@ -140,10 +148,13 @@ def get_system_proxy_config() -> dict[str, Any] | None:
|
|||||||
"""
|
"""
|
||||||
global _system_proxy_cache
|
global _system_proxy_cache
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if _system_proxy_cache:
|
|
||||||
value, expires_at = _system_proxy_cache
|
# 快速路径:缓存命中
|
||||||
if now < expires_at:
|
with _system_proxy_cache_lock:
|
||||||
return value
|
if _system_proxy_cache:
|
||||||
|
value, expires_at = _system_proxy_cache
|
||||||
|
if now < expires_at:
|
||||||
|
return value
|
||||||
|
|
||||||
from src.database import create_session
|
from src.database import create_session
|
||||||
from src.services.system.config import SystemConfigService
|
from src.services.system.config import SystemConfigService
|
||||||
@@ -155,15 +166,21 @@ def get_system_proxy_config() -> dict[str, Any] | None:
|
|||||||
result: dict[str, Any] | None = {"node_id": node_id.strip(), "enabled": True}
|
result: dict[str, Any] | None = {"node_id": node_id.strip(), "enabled": True}
|
||||||
else:
|
else:
|
||||||
result = None
|
result = None
|
||||||
_system_proxy_cache = (result, now + _SYSTEM_PROXY_CACHE_TTL)
|
|
||||||
return result
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("获取系统默认代理配置失败: {}", exc)
|
logger.warning("获取系统默认代理配置失败: {}", exc)
|
||||||
_system_proxy_cache = (None, now + _SYSTEM_PROXY_CACHE_TTL)
|
result = None
|
||||||
return None
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
with _system_proxy_cache_lock:
|
||||||
|
_system_proxy_cache = (result, now + _SYSTEM_PROXY_CACHE_TTL)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def get_system_proxy_config_async() -> dict[str, Any] | None:
|
||||||
|
"""异步读取系统默认代理配置,避免在事件循环中执行同步 DB 查询。"""
|
||||||
|
return await asyncio.to_thread(get_system_proxy_config)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 代理 URL 认证注入
|
# 代理 URL 认证注入
|
||||||
@@ -472,6 +489,13 @@ def build_proxy_url(proxy_config: dict[str, Any]) -> str | None:
|
|||||||
return proxy_url
|
return proxy_url
|
||||||
|
|
||||||
|
|
||||||
|
async def build_proxy_url_async(proxy_config: dict[str, Any] | None) -> str | None:
|
||||||
|
"""异步构建代理 URL,避免 ProxyNode 查询阻塞事件循环。"""
|
||||||
|
if not proxy_config:
|
||||||
|
return None
|
||||||
|
return await asyncio.to_thread(build_proxy_url, proxy_config)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 代理信息追踪(日志/usage)
|
# 代理信息追踪(日志/usage)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -528,6 +552,11 @@ def resolve_proxy_info(proxy_config: dict[str, Any] | None) -> dict[str, Any] |
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_proxy_info_async(proxy_config: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||||
|
"""异步解析代理摘要信息,避免事件循环被同步代理解析阻塞。"""
|
||||||
|
return await asyncio.to_thread(resolve_proxy_info, proxy_config)
|
||||||
|
|
||||||
|
|
||||||
def get_proxy_label(proxy_info: dict[str, Any] | None) -> str:
|
def get_proxy_label(proxy_info: dict[str, Any] | None) -> str:
|
||||||
"""从 proxy_info 中提取简短的代理标签(用于日志)"""
|
"""从 proxy_info 中提取简短的代理标签(用于日志)"""
|
||||||
if not proxy_info:
|
if not proxy_info:
|
||||||
@@ -612,6 +641,13 @@ def resolve_delegate_config(proxy_config: dict[str, Any] | None) -> dict[str, An
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_delegate_config_async(
|
||||||
|
proxy_config: dict[str, Any] | None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""异步解析 tunnel 代理配置,避免同步 DB 查询阻塞事件循环。"""
|
||||||
|
return await asyncio.to_thread(resolve_delegate_config, proxy_config)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 统一上游请求参数构建
|
# 统一上游请求参数构建
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -642,6 +678,25 @@ def _maybe_compress_payload(
|
|||||||
return json_bytes, normalized_headers
|
return json_bytes, normalized_headers
|
||||||
|
|
||||||
|
|
||||||
|
async def _maybe_compress_payload_async(
|
||||||
|
payload: Any,
|
||||||
|
headers: dict[str, str],
|
||||||
|
client_content_encoding: str | None = None,
|
||||||
|
) -> tuple[bytes, dict[str, str]]:
|
||||||
|
"""异步版 _maybe_compress_payload,仅在 payload 超过阈值时走线程池。"""
|
||||||
|
json_bytes = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||||
|
normalized_headers = {k: v for k, v in headers.items() if k.lower() != "content-encoding"}
|
||||||
|
|
||||||
|
if is_gzip_content_encoding(client_content_encoding):
|
||||||
|
if len(json_bytes) >= _ASYNC_PAYLOAD_THRESHOLD:
|
||||||
|
compressed = await asyncio.to_thread(gzip.compress, json_bytes, 6)
|
||||||
|
else:
|
||||||
|
compressed = gzip.compress(json_bytes, compresslevel=6)
|
||||||
|
return compressed, {**normalized_headers, "Content-Encoding": "gzip"}
|
||||||
|
|
||||||
|
return json_bytes, normalized_headers
|
||||||
|
|
||||||
|
|
||||||
def build_post_kwargs(
|
def build_post_kwargs(
|
||||||
_delegate_cfg: dict[str, Any] | None = None,
|
_delegate_cfg: dict[str, Any] | None = None,
|
||||||
*,
|
*,
|
||||||
@@ -673,6 +728,28 @@ def build_post_kwargs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def build_post_kwargs_async(
|
||||||
|
_delegate_cfg: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
url: str,
|
||||||
|
headers: dict[str, str],
|
||||||
|
payload: Any,
|
||||||
|
timeout: float,
|
||||||
|
client_content_encoding: str | None = None,
|
||||||
|
refresh_auth: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""构建 POST kwargs,大 payload 的序列化/压缩走线程池避免阻塞事件循环。"""
|
||||||
|
content, final_headers = await _maybe_compress_payload_async(
|
||||||
|
payload, headers, client_content_encoding
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"content": content,
|
||||||
|
"headers": final_headers,
|
||||||
|
"timeout": httpx.Timeout(timeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_stream_kwargs(
|
def build_stream_kwargs(
|
||||||
_delegate_cfg: dict[str, Any] | None = None,
|
_delegate_cfg: dict[str, Any] | None = None,
|
||||||
*,
|
*,
|
||||||
@@ -704,3 +781,27 @@ def build_stream_kwargs(
|
|||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
kwargs["timeout"] = httpx.Timeout(timeout)
|
kwargs["timeout"] = httpx.Timeout(timeout)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
async def build_stream_kwargs_async(
|
||||||
|
_delegate_cfg: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
url: str,
|
||||||
|
headers: dict[str, str],
|
||||||
|
payload: Any,
|
||||||
|
timeout: float | None = None,
|
||||||
|
client_content_encoding: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""构建 stream kwargs,大 payload 的序列化/压缩走线程池避免阻塞事件循环。"""
|
||||||
|
content, final_headers = await _maybe_compress_payload_async(
|
||||||
|
payload, headers, client_content_encoding
|
||||||
|
)
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"method": "POST",
|
||||||
|
"url": url,
|
||||||
|
"content": content,
|
||||||
|
"headers": final_headers,
|
||||||
|
}
|
||||||
|
if timeout is not None:
|
||||||
|
kwargs["timeout"] = httpx.Timeout(timeout)
|
||||||
|
return kwargs
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class RequestExecutor:
|
|||||||
# 非流式请求:标记为 success 状态
|
# 非流式请求:标记为 success 状态
|
||||||
from src.services.proxy_node.resolver import (
|
from src.services.proxy_node.resolver import (
|
||||||
resolve_effective_proxy,
|
resolve_effective_proxy,
|
||||||
resolve_proxy_info,
|
resolve_proxy_info_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
_eff_proxy = resolve_effective_proxy(
|
_eff_proxy = resolve_effective_proxy(
|
||||||
@@ -214,7 +214,7 @@ class RequestExecutor:
|
|||||||
"model_name": model_name,
|
"model_name": model_name,
|
||||||
"api_format": api_format,
|
"api_format": api_format,
|
||||||
}
|
}
|
||||||
_pi = resolve_proxy_info(_eff_proxy)
|
_pi = await resolve_proxy_info_async(_eff_proxy)
|
||||||
if _pi:
|
if _pi:
|
||||||
_extra["proxy"] = _pi
|
_extra["proxy"] = _pi
|
||||||
RequestCandidateService.mark_candidate_success(
|
RequestCandidateService.mark_candidate_success(
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ class TaskErrorOperationsService:
|
|||||||
- "raise": raise the underlying exception
|
- "raise": raise the underlying exception
|
||||||
"""
|
"""
|
||||||
from src.core.api_format.conversion.exceptions import FormatConversionError
|
from src.core.api_format.conversion.exceptions import FormatConversionError
|
||||||
from src.services.proxy_node.resolver import resolve_effective_proxy, resolve_proxy_info
|
from src.services.proxy_node.resolver import (
|
||||||
|
resolve_effective_proxy,
|
||||||
|
resolve_proxy_info_async,
|
||||||
|
)
|
||||||
from src.services.request.executor import ExecutionError
|
from src.services.request.executor import ExecutionError
|
||||||
|
|
||||||
# 提前解析代理信息,写入候选记录的 extra_data(用于链路追踪展示)
|
# 提前解析代理信息,写入候选记录的 extra_data(用于链路追踪展示)
|
||||||
@@ -207,7 +210,7 @@ class TaskErrorOperationsService:
|
|||||||
getattr(candidate.provider, "proxy", None),
|
getattr(candidate.provider, "proxy", None),
|
||||||
getattr(candidate.key, "proxy", None),
|
getattr(candidate.key, "proxy", None),
|
||||||
)
|
)
|
||||||
_proxy_info = resolve_proxy_info(_eff_proxy)
|
_proxy_info = await resolve_proxy_info_async(_eff_proxy)
|
||||||
_proxy_extra: dict[str, Any] | None = {"proxy": _proxy_info} if _proxy_info else None
|
_proxy_extra: dict[str, Any] | None = {"proxy": _proxy_info} if _proxy_info else None
|
||||||
|
|
||||||
if not isinstance(exec_err, ExecutionError):
|
if not isinstance(exec_err, ExecutionError):
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ from __future__ import annotations
|
|||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from src.services.proxy_node.resolver import build_post_kwargs, build_stream_kwargs
|
import pytest
|
||||||
|
|
||||||
|
from src.services.proxy_node.resolver import (
|
||||||
|
build_post_kwargs,
|
||||||
|
build_post_kwargs_async,
|
||||||
|
build_stream_kwargs,
|
||||||
|
build_stream_kwargs_async,
|
||||||
|
resolve_proxy_info_async,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestProxyResolverCompression:
|
class TestProxyResolverCompression:
|
||||||
@@ -46,3 +54,46 @@ class TestProxyResolverCompression:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert all(key.lower() != "content-encoding" for key in kwargs["headers"])
|
assert all(key.lower() != "content-encoding" for key in kwargs["headers"])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_post_kwargs_async_compresses_when_client_sent_gzip(self) -> None:
|
||||||
|
payload = {"message": "hello", "tokens": [1, 2, 3]}
|
||||||
|
|
||||||
|
kwargs = await build_post_kwargs_async(
|
||||||
|
None,
|
||||||
|
url="https://example.com/v1/messages",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
payload=payload,
|
||||||
|
timeout=10.0,
|
||||||
|
client_content_encoding="gzip",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert kwargs["headers"]["Content-Encoding"] == "gzip"
|
||||||
|
assert json.loads(gzip.decompress(kwargs["content"]).decode("utf-8")) == payload
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_stream_kwargs_async_drops_stale_content_encoding_header(self) -> None:
|
||||||
|
kwargs = await build_stream_kwargs_async(
|
||||||
|
None,
|
||||||
|
url="https://example.com/v1/messages",
|
||||||
|
headers={"content-encoding": "gzip", "Content-Type": "application/json"},
|
||||||
|
payload={"message": "no-gzip"},
|
||||||
|
timeout=10.0,
|
||||||
|
client_content_encoding=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert all(key.lower() != "content-encoding" for key in kwargs["headers"])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_proxy_info_async_masks_manual_proxy_url(self) -> None:
|
||||||
|
info = await resolve_proxy_info_async(
|
||||||
|
{
|
||||||
|
"enabled": True,
|
||||||
|
"url": "socks5://user:pass@proxy.example.com:1080",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert info == {
|
||||||
|
"url": "socks5://proxy.example.com:1080",
|
||||||
|
"source": "provider",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user