单源 + 上游 429 无 Retry-After 时,一次调用挂满整个 stall window(生产配置下 >20 分钟) #22

Open
opened 2026-09-05 22:37:30 +08:00 by iomgaa · 0 comments
Owner

一句话

单源场景下,上游回 429 且不带 Retry-After 时,一次调用会挂满整个 stall window——生产配置 LLM__BACKPRESSURE__STALL_WINDOW_S=1200 下,每轮阻塞超过 20 分钟才报 stalled

怎么发现的

2026-09-05 跑 T10 能力表实测(经 new-api 对 26 个模型逐个打真实请求)时撞到: claude 系三个型号的 key 撞上「7 天限额已用完」、gpt-5.4 全账号限流,上游一律回 429 且不带 Retry-After。探测用例因此每个型号卡住约 20 分钟,只能把 stall window 临时压到 60s 绕开。

压到 45s 时实测 46.7s 报 stalled,路径已确认。

为什么值得单独修

  • 与推理档位无关,是既有的背压/重试行为,任何走单源 + 上游持续 429 的下游都会撞上;
  • 生产默认配置(1200s)正是为「重试预算不被 stall 判死吃掉」而设的(见 issue #8),两者在这个场景下互相拉扯: 窗口调小会让正常重试被误判 stalled,窗口调大则 429 时长时间不返回;
  • 429 是上游明确表态「现在别打了」,与「网络卡住、一个字节都没回来」在语义上完全不同,却共用同一条 stall 判定路径。

复现

单源(LLM__X__1__*,不配第二个源),让上游稳定回 429 且响应头无 Retry-After,发一次 chat()。观察: 直到 stall window 耗尽才返回。

可能的方向(未定)

  1. 429 且无 Retry-After 时用退避上限(LLM_RETRY_MAX_DELAY)而不是 stall window 兜底;
  2. TransientError.retry_after_s 在缺失时取一个保守默认,而不是留空让上层等满;
  3. 单源场景专门处理: 无备源可换时,重试预算耗尽即返回,不再等 stall window。

选型需评估与 issue #8(stall window 必须 > TIMEOUT_S × MAX_RETRIES)的相互作用。

现场数据

  • 生产窗口 1200s: 每轮阻塞 > 20 分钟
  • 窗口 45s: 46.7s 报 stalled
  • 触发型号: claude-opus-5 / claude-sonnet-5 / claude-haiku-5(7 天限额)、gpt-5.4(全账号限流)
## 一句话 单源场景下,上游回 429 且不带 `Retry-After` 时,一次调用会挂满整个 stall window——生产配置 `LLM__BACKPRESSURE__STALL_WINDOW_S=1200` 下,每轮阻塞超过 20 分钟才报 `stalled`。 ## 怎么发现的 2026-09-05 跑 T10 能力表实测(经 new-api 对 26 个模型逐个打真实请求)时撞到: claude 系三个型号的 key 撞上「7 天限额已用完」、gpt-5.4 全账号限流,上游一律回 429 且不带 `Retry-After`。探测用例因此每个型号卡住约 20 分钟,只能把 stall window 临时压到 60s 绕开。 压到 45s 时实测 46.7s 报 `stalled`,路径已确认。 ## 为什么值得单独修 - **与推理档位无关**,是既有的背压/重试行为,任何走单源 + 上游持续 429 的下游都会撞上; - 生产默认配置(1200s)正是为「重试预算不被 stall 判死吃掉」而设的(见 issue #8),两者在这个场景下互相拉扯: 窗口调小会让正常重试被误判 stalled,窗口调大则 429 时长时间不返回; - 429 是**上游明确表态**「现在别打了」,与「网络卡住、一个字节都没回来」在语义上完全不同,却共用同一条 stall 判定路径。 ## 复现 单源(`LLM__X__1__*`,不配第二个源),让上游稳定回 429 且响应头无 `Retry-After`,发一次 `chat()`。观察: 直到 stall window 耗尽才返回。 ## 可能的方向(未定) 1. 429 且无 `Retry-After` 时用退避上限(`LLM_RETRY_MAX_DELAY`)而不是 stall window 兜底; 2. 让 `TransientError.retry_after_s` 在缺失时取一个保守默认,而不是留空让上层等满; 3. 单源场景专门处理: 无备源可换时,重试预算耗尽即返回,不再等 stall window。 选型需评估与 issue #8(stall window 必须 > TIMEOUT_S × MAX_RETRIES)的相互作用。 ## 现场数据 - 生产窗口 1200s: 每轮阻塞 > 20 分钟 - 窗口 45s: 46.7s 报 `stalled` - 触发型号: claude-opus-5 / claude-sonnet-5 / claude-haiku-5(7 天限额)、gpt-5.4(全账号限流)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#22