feat: cmhub shared Session + connection pool + proxy handling (T-546)
- app/ai.py: 模块级共享 requests.Session + HTTPAdapter 连接池 (CMHUB_HTTP_POOL_SIZE=32);_apply_cmhub_proxy() 切换 trust_env; 生成/下载/models/balance 统一复用该 Session。 - app/appconfig.py: ai.cmhub.use_system_proxy 默认 false(绕过系统代理)。 - tests/test_ai.py: 补 Session 复用/代理/连接池测试;原 18 处 cmhub mock 从模块级 requests 改到共享 Session。 - docs: troubleshooting 加「图片下载很慢」排障节;06-tasks 标 T-546 DONE。 附带:把 app/ai.py 行尾从 CRLF 归一为 LF;并一并纳入工作区中此前未提交、 已在运行构建里的 cmhub 下载诊断代码(调试图片 URL 日志、下载重试事件), 无法与 T-546 在同一文件内拆分单独提交。243 单测全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b257fb5c83
commit
5a09c6662e
@@ -16,6 +16,7 @@ import urllib.request
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from . import appconfig, db, diagnostics, image_paths
|
||||
from . import prompts as prompt_module
|
||||
@@ -40,6 +41,50 @@ CMHUB_IMAGE_MAX_BYTES = 20 * 1024 * 1024
|
||||
CMHUB_TITLE_READ_TIMEOUT_SECONDS = 600
|
||||
CMHUB_IMAGE_READ_TIMEOUT_SECONDS = 650
|
||||
CMHUB_IMAGE_CONCURRENCY_LIMIT = 5
|
||||
CMHUB_IMAGE_DOWNLOAD_ATTEMPTS = 3
|
||||
CMHUB_IMAGE_SLOW_DOWNLOAD_SECONDS = 20.0
|
||||
# 连接池要覆盖“生图并发 + 下载并发”(各上限 5)再留余量,避免连接不足排队或
|
||||
# urllib3 "Connection pool is full" 警告。
|
||||
CMHUB_HTTP_POOL_SIZE = 32
|
||||
|
||||
_CMHUB_SESSION = None
|
||||
_CMHUB_SESSION_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _cmhub_session():
|
||||
"""返回 cmhub 专用的共享 `requests.Session`(连接复用 + 连接池)。
|
||||
|
||||
所有 cmhub HTTP(生成/下载/models/balance)都走同一个 Session,避免每次调用
|
||||
新建 TCP+TLS 造成握手风暴与 connect_timeout;跨线程发请求安全,连接池足够大即可。
|
||||
"""
|
||||
|
||||
global _CMHUB_SESSION
|
||||
if _CMHUB_SESSION is None:
|
||||
with _CMHUB_SESSION_LOCK:
|
||||
if _CMHUB_SESSION is None:
|
||||
session = requests.Session()
|
||||
adapter = HTTPAdapter(
|
||||
pool_connections=CMHUB_HTTP_POOL_SIZE,
|
||||
pool_maxsize=CMHUB_HTTP_POOL_SIZE,
|
||||
max_retries=0,
|
||||
)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
_CMHUB_SESSION = session
|
||||
return _CMHUB_SESSION
|
||||
|
||||
|
||||
def _apply_cmhub_proxy(use_system_proxy):
|
||||
"""设置 cmhub Session 是否读取系统代理环境变量。
|
||||
|
||||
`use_system_proxy=False` → `trust_env=False`,忽略 `HTTP(S)_PROXY`/`ALL_PROXY` 直连;
|
||||
公网 cmhub 网关默认直连,避免误走慢代理导致明文图片下载卡住。需要代理的环境可在
|
||||
`config.json` 的 `ai.cmhub.use_system_proxy` 打开。
|
||||
"""
|
||||
|
||||
session = _cmhub_session()
|
||||
session.trust_env = bool(use_system_proxy)
|
||||
return session
|
||||
|
||||
_RESOLUTION_SIZES = {
|
||||
"512": (512, 512),
|
||||
@@ -233,7 +278,16 @@ def _notify_step(callback, step):
|
||||
pass
|
||||
|
||||
|
||||
def _notify_step_event(callback, step, result="success", detail=None, level="info"):
|
||||
def _notify_step_event(
|
||||
callback,
|
||||
step,
|
||||
result="success",
|
||||
detail=None,
|
||||
level="info",
|
||||
debug_only=False,
|
||||
attempt=None,
|
||||
attempts=None,
|
||||
):
|
||||
if callback is None:
|
||||
return
|
||||
payload = {
|
||||
@@ -243,6 +297,12 @@ def _notify_step_event(callback, step, result="success", detail=None, level="inf
|
||||
}
|
||||
if detail is not None:
|
||||
payload["detail"] = detail
|
||||
if attempt is not None:
|
||||
payload["attempt"] = attempt
|
||||
if attempts is not None:
|
||||
payload["attempts"] = attempts
|
||||
if debug_only:
|
||||
payload["debug_only"] = True
|
||||
try:
|
||||
callback(payload)
|
||||
except Exception:
|
||||
@@ -421,6 +481,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
attempt=event.get("attempt"),
|
||||
attempts=event.get("attempts"),
|
||||
metadata=event.get("metadata"),
|
||||
debug_only=event.get("debug_only"),
|
||||
)
|
||||
return
|
||||
set_step(task, event)
|
||||
@@ -703,7 +764,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
return summary
|
||||
|
||||
|
||||
def fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30):
|
||||
def fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30, use_system_proxy=False):
|
||||
"""Fetch cmhub model aliases for settings UI."""
|
||||
|
||||
base_url = str(base_url or "").strip()
|
||||
@@ -712,6 +773,7 @@ def fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30):
|
||||
raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub Base URL")
|
||||
if not api_key:
|
||||
raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub API Key")
|
||||
_apply_cmhub_proxy(use_system_proxy)
|
||||
data = _cmhub_call_with_retry(
|
||||
"GET",
|
||||
appconfig.cmhub_request_url(base_url, "/api/v1/models"),
|
||||
@@ -728,7 +790,7 @@ def fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30):
|
||||
return [copy.deepcopy(model) for model in models if isinstance(model, dict)]
|
||||
|
||||
|
||||
def fetch_cmhub_balance(base_url, api_key, connect_timeout=10, read_timeout=30):
|
||||
def fetch_cmhub_balance(base_url, api_key, connect_timeout=10, read_timeout=30, use_system_proxy=False):
|
||||
"""Fetch cmhub point balance for settings UI."""
|
||||
|
||||
base_url = str(base_url or "").strip()
|
||||
@@ -737,6 +799,7 @@ def fetch_cmhub_balance(base_url, api_key, connect_timeout=10, read_timeout=30):
|
||||
raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub Base URL")
|
||||
if not api_key:
|
||||
raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub API Key")
|
||||
_apply_cmhub_proxy(use_system_proxy)
|
||||
data = _cmhub_call_with_retry(
|
||||
"GET",
|
||||
appconfig.cmhub_request_url(base_url, "/api/v1/balance"),
|
||||
@@ -894,6 +957,15 @@ def _request_cmhub_cover_image(
|
||||
"cover_request",
|
||||
detail="cmhub 已返回 image_url,耗时 %s" % _format_seconds(request_elapsed),
|
||||
)
|
||||
if _debug_cmhub_image_url_enabled():
|
||||
_notify_step_event(
|
||||
on_step,
|
||||
"cover_image_url",
|
||||
result="debug",
|
||||
detail="cmhub 图片 URL:%s" % image_url,
|
||||
level="warning",
|
||||
debug_only=True,
|
||||
)
|
||||
return {
|
||||
"image_url": image_url,
|
||||
"connect_timeout": runtime["connect_timeout"],
|
||||
@@ -912,13 +984,12 @@ def _download_and_save_cmhub_cover(request_result, on_step=None):
|
||||
resolution = request_result["resolution"]
|
||||
quality = request_result["quality"]
|
||||
_notify_step(on_step, "cover_download")
|
||||
download_started = time.perf_counter()
|
||||
image_bytes = _download_cmhub_image(
|
||||
image_bytes, download_elapsed = _download_cmhub_image_with_retry(
|
||||
image_url,
|
||||
connect_timeout=connect_timeout,
|
||||
read_timeout=read_timeout,
|
||||
on_step=on_step,
|
||||
)
|
||||
download_elapsed = time.perf_counter() - download_started
|
||||
_notify_step_event(
|
||||
on_step,
|
||||
"cover_download",
|
||||
@@ -955,11 +1026,14 @@ def _cmhub_runtime(config, operation, cmhub_config_path):
|
||||
"请去⑤设置配置 cmhub:缺少 " + "、".join(missing),
|
||||
retryable=False,
|
||||
)
|
||||
use_system_proxy = bool(hub.get("use_system_proxy", False))
|
||||
_apply_cmhub_proxy(use_system_proxy)
|
||||
return {
|
||||
"base_url": hub["base_url"].rstrip("/"),
|
||||
"api_key": api_key,
|
||||
"alias": hub[alias_key],
|
||||
"connect_timeout": max(1, int(hub.get("connect_timeout", 10) or 10)),
|
||||
"use_system_proxy": use_system_proxy,
|
||||
}
|
||||
|
||||
|
||||
@@ -992,6 +1066,73 @@ def _cmhub_read_timeout(config, resolution):
|
||||
return CMHUB_IMAGE_READ_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
def _debug_cmhub_image_url_enabled():
|
||||
value = str(os.environ.get("CMSHOPEE_DEBUG_CMHUB_IMAGE_URL", "") or "")
|
||||
return value.strip().lower() in {"1", "true", "yes", "on", "debug"}
|
||||
|
||||
|
||||
def _download_cmhub_image_with_retry(
|
||||
url,
|
||||
connect_timeout,
|
||||
read_timeout,
|
||||
on_step=None,
|
||||
attempts=CMHUB_IMAGE_DOWNLOAD_ATTEMPTS,
|
||||
slow_threshold=CMHUB_IMAGE_SLOW_DOWNLOAD_SECONDS,
|
||||
):
|
||||
total_attempts = max(1, int(attempts or 1))
|
||||
total_started = time.perf_counter()
|
||||
last_exc = None
|
||||
for index in range(total_attempts):
|
||||
try:
|
||||
image_bytes = _download_cmhub_image(
|
||||
url,
|
||||
connect_timeout=connect_timeout,
|
||||
read_timeout=read_timeout,
|
||||
)
|
||||
elapsed = time.perf_counter() - total_started
|
||||
if elapsed >= float(slow_threshold or 0):
|
||||
_notify_step_event(
|
||||
on_step,
|
||||
"cover_download",
|
||||
result="warning",
|
||||
detail="图片下载较慢,已用 %s,大小 %s"
|
||||
% (_format_seconds(elapsed), _format_bytes(len(image_bytes))),
|
||||
level="warning",
|
||||
)
|
||||
return image_bytes, elapsed
|
||||
except Exception as exc:
|
||||
last_exc = exc
|
||||
if index + 1 >= total_attempts or not _cmhub_download_retryable(exc):
|
||||
break
|
||||
_notify_step_event(
|
||||
on_step,
|
||||
"cover_download",
|
||||
result="retry",
|
||||
detail=str(exc),
|
||||
level="warning",
|
||||
attempt=index + 1,
|
||||
attempts=total_attempts,
|
||||
)
|
||||
time.sleep(min(2.0, 0.5 * (index + 1)))
|
||||
if total_attempts > 1 and _cmhub_download_retryable(last_exc):
|
||||
raise AIError(
|
||||
"下载 cmhub 图片失败(已尝试 %s 次): %s"
|
||||
% (total_attempts, str(last_exc))
|
||||
) from last_exc
|
||||
raise last_exc
|
||||
|
||||
|
||||
def _cmhub_download_retryable(exc):
|
||||
if exc is None:
|
||||
return False
|
||||
message = str(exc or "")
|
||||
if "图片超过大小上限" in message:
|
||||
return False
|
||||
if "HTTP 4" in message:
|
||||
return False
|
||||
return message.startswith("下载 cmhub 图片失败")
|
||||
|
||||
|
||||
def _cmhub_call_with_retry(
|
||||
method,
|
||||
url,
|
||||
@@ -1039,7 +1180,7 @@ def _cmhub_call_once(method, url, api_key, payload, connect_timeout, read_timeou
|
||||
if str(method).upper() != "GET":
|
||||
request_kwargs["json"] = payload or {}
|
||||
try:
|
||||
response = requests.request(str(method).upper(), url, **request_kwargs)
|
||||
response = _cmhub_session().request(str(method).upper(), url, **request_kwargs)
|
||||
except requests.exceptions.ConnectTimeout as exc:
|
||||
raise CMHubError("connect_timeout", "连接 cmhub 超时", retryable=True) from exc
|
||||
except requests.exceptions.ReadTimeout as exc:
|
||||
@@ -1208,7 +1349,7 @@ def _format_bytes(size):
|
||||
def _download_cmhub_image(url, connect_timeout, read_timeout, max_bytes=CMHUB_IMAGE_MAX_BYTES):
|
||||
_assert_public_http_url(url)
|
||||
try:
|
||||
response = requests.get(
|
||||
response = _cmhub_session().get(
|
||||
url,
|
||||
stream=True,
|
||||
timeout=(max(1, int(connect_timeout)), max(1, int(read_timeout))),
|
||||
@@ -1379,6 +1520,7 @@ def _emit_generation_event(
|
||||
attempt=None,
|
||||
attempts=None,
|
||||
metadata=None,
|
||||
debug_only=None,
|
||||
):
|
||||
if callback is None:
|
||||
return
|
||||
@@ -1397,6 +1539,8 @@ def _emit_generation_event(
|
||||
payload["attempts"] = attempts
|
||||
if metadata is not None:
|
||||
payload["metadata"] = appconfig.sanitize_for_log(metadata)
|
||||
if debug_only:
|
||||
payload["debug_only"] = True
|
||||
try:
|
||||
callback(payload)
|
||||
except Exception:
|
||||
|
||||
@@ -78,6 +78,7 @@ DEFAULT_CONFIG = {
|
||||
"title_alias": "",
|
||||
"image_alias": "",
|
||||
"connect_timeout": 10,
|
||||
"use_system_proxy": False,
|
||||
"check_balance_before_batch": False,
|
||||
},
|
||||
"title_concurrency": 4,
|
||||
|
||||
Reference in New Issue
Block a user