timeout_config.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """统一超时配置(修永久卡死).
  2. 集中各阶段"单次外部调用"的总时长上限(用户拍板),并提供 httpx.Timeout 工厂。
  3. 要点:httpx 的 `timeout=标量` 只把 connect/read/write/pool 各设为 N,**没有"整次请求总时长"**;
  4. 服务端慢速吐字节时每次 read 都在 N 内返回一点 → read 永不触发 → 永久卡在 do_poll。
  5. 所以这里强制 **read 相设短**(停止吐数据即抛 ReadTimeout),总时长由 write 相 + 调用方护栏兜。
  6. env 可覆盖各阶段总值,但被硬上限钳制,防再配出 3600 那种值。
  7. """
  8. from __future__ import annotations
  9. import os
  10. from typing import Mapping
  11. import httpx
  12. CONNECT_TIMEOUT_SECONDS = 10.0
  13. # 各阶段总时长默认(秒)——用户拍板的"单次外部调用允许上限"。
  14. _DEFAULTS: dict[str, float] = {
  15. "oss": 300.0, # OSS 上传/归档单次尝试 5min
  16. "video_download": 600.0, # 视频下载 10min
  17. "video_llm": 600.0, # qwen/gemini 单次判定 10min
  18. "crawapi": 180.0, # 平台搜索/作者/画像 3min
  19. "query_llm": 120.0, # query variant / Gate2 2min
  20. "pg": 30.0, # pattern PG Gate1 30s
  21. }
  22. # env 覆盖也不得超过(防误配)。
  23. _HARD_CEILING: dict[str, float] = {
  24. "oss": 600.0,
  25. "video_download": 1200.0,
  26. "video_llm": 1200.0,
  27. "crawapi": 360.0,
  28. "query_llm": 300.0,
  29. "pg": 60.0,
  30. }
  31. # 单次 read(两次收到数据之间)上限——短,杜绝 do_poll 永久阻塞。
  32. _READ: dict[str, float] = {
  33. "oss": 60.0,
  34. "video_download": 120.0,
  35. "video_llm": 120.0,
  36. "crawapi": 60.0,
  37. "query_llm": 60.0,
  38. "pg": 30.0,
  39. }
  40. _ENV_KEYS: dict[str, tuple[str, ...]] = {
  41. "oss": ("CONTENT_AGENT_OSS_TIMEOUT_SECONDS",),
  42. "video_download": ("CONTENT_AGENT_VIDEO_DOWNLOAD_TIMEOUT_SECONDS",),
  43. "video_llm": ("CONTENT_AGENT_VIDEO_LLM_TIMEOUT_SECONDS",),
  44. "crawapi": ("CONTENTFIND_API_CRAWAPI_TIMEOUT_SECONDS",),
  45. "query_llm": ("CONTENT_AGENT_QUERY_LLM_TIMEOUT_SECONDS",),
  46. "pg": ("OPEN_AIGC_PG_TIMEOUT_SECONDS",),
  47. }
  48. def total_timeout(stage: str, env: Mapping[str, str] | None = None) -> float:
  49. """阶段总时长(秒):env 覆盖 → 硬上限钳制 → 默认。"""
  50. src = os.environ if env is None else env
  51. value = _DEFAULTS[stage]
  52. for key in _ENV_KEYS[stage]:
  53. raw = src.get(key)
  54. if raw:
  55. try:
  56. value = float(raw)
  57. break
  58. except (TypeError, ValueError):
  59. pass
  60. return min(value, _HARD_CEILING[stage])
  61. def read_timeout(stage: str) -> float:
  62. return _READ[stage]
  63. def as_httpx_timeout(
  64. total_seconds: float,
  65. *,
  66. read: float,
  67. connect: float = CONNECT_TIMEOUT_SECONDS,
  68. ) -> httpx.Timeout:
  69. """把一个总时长(秒)转成分段 httpx.Timeout:read 短、write=总、connect 短。"""
  70. total = max(float(total_seconds), 1.0)
  71. return httpx.Timeout(
  72. connect=min(connect, total),
  73. read=min(read, total),
  74. write=total,
  75. pool=min(connect, total),
  76. )
  77. def httpx_timeout(stage: str, env: Mapping[str, str] | None = None) -> httpx.Timeout:
  78. """按阶段直接构造 httpx.Timeout(已含 env 覆盖 + read 短上限)。"""
  79. return as_httpx_timeout(total_timeout(stage, env=env), read=_READ[stage])