docs: sharpen the productive-time boundary after Codex review
Two internal-consistency fixes from the independent design review: the 429 saturation argument wrongly claimed exponential backoff growth (429 skips the retry budget, so max(fails, 1) pins the delay to the base tier), and "productive" was defined as waiting on the response while the StallClock actually wraps all of _attempt. The boundary is now stated as _attempt itself, including per-attempt accounting and telemetry, with the rationale that telemetry jitter must not participate in the stall verdict.
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
| 时间性质 | 构成 | 应由谁治理 | 耗尽后 |
|
| 时间性质 | 构成 | 应由谁治理 | 耗尽后 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **生产性** | 真实发出请求并等待响应(含耗满 `timeout_s` 的超时、TTFT 等待、流式读取) | `max_attempts`(重试预算) | `retry_exhausted` |
|
| **生产性** | 一次尝试的完整生命周期(发请求、等响应含耗满 `timeout_s` 的超时/TTFT/流式读取,以及该次尝试的记账与遥测收尾) | `max_attempts`(重试预算) | `retry_exhausted` |
|
||||||
| **非生产性** | 429 退避、配额 wait 轮询、熔断冷却轮询、AIMD 排队 | **无人治理**(429 不计 `fails`)→ 正是 stall 的职责 | `stalled` |
|
| **非生产性** | 429 退避、配额 wait 轮询、熔断冷却轮询、AIMD 排队 | **无人治理**(429 不计 `fails`)→ 正是 stall 的职责 | `stalled` |
|
||||||
|
|
||||||
**缺陷即:生产性时间同时向两个预算计费。** 而 stall 预算(默认 300s)远小于重试预算(`3 × 300s`),必然先耗尽,于是重试预算在超时场景下**永远用不上**——issue 观察到的"静默失效"就是这个重叠计费的直接后果。
|
**缺陷即:生产性时间同时向两个预算计费。** 而 stall 预算(默认 300s)远小于重试预算(`3 × 300s`),必然先耗尽,于是重试预算在超时场景下**永远用不上**——issue 观察到的"静默失效"就是这个重叠计费的直接后果。
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
| 真实尝试(`_attempt` 内) | 重试预算 `max_attempts` |
|
| 真实尝试(`_attempt` 内) | 重试预算 `max_attempts` |
|
||||||
| 其余一切等待 | stall 预算 `stall_window_s` |
|
| 其余一切等待 | stall 预算 `stall_window_s` |
|
||||||
|
|
||||||
|
**"生产性"的边界即 `_attempt` 的边界**——包含该次尝试的记账(`record_success`/`mark_progress`)与遥测收尾,而不止于"等响应"。这是有意的:这些收尾是"尝试已有结论"之后的动作,不是"在等待重试机会"的停滞;把它们计入 stall 会让遥测抖动参与判死,与「遥测写失败降级不冒泡」所守的"遥测不得影响主路径判决"同精神。其耗时本也在毫秒量级。
|
||||||
|
|
||||||
这与库内既有原则**同构**:429 不烧重试预算,所以 429 等待烧 stall 预算;真实尝试烧重试预算,所以它不烧 stall 预算。
|
这与库内既有原则**同构**:429 不烧重试预算,所以 429 等待烧 stall 预算;真实尝试烧重试预算,所以它不烧 stall 预算。
|
||||||
|
|
||||||
### 3.2 为什么取补集,而不是逐处标记 sleep
|
### 3.2 为什么取补集,而不是逐处标记 sleep
|
||||||
@@ -127,7 +129,9 @@ async with clock.attempting(): # 包裹真实尝试
|
|||||||
|
|
||||||
### 3.5 429 饱和场景下兜底仍然有效(正确性验证)
|
### 3.5 429 饱和场景下兜底仍然有效(正确性验证)
|
||||||
|
|
||||||
修改后必须确认 stall 兜底没有被削弱:429 往返本身是生产性时间,不再计入 stall。但一次 429 往返是**快速失败**(网关立即拒绝,不耗 `timeout_s`),而其后的退避 sleep 是非生产性的且随尝试次数指数增长。故饱和期内非生产性时间占绝对多数,`stalled_s` 仍会在接近 `stall_window_s` 的时间内累满 —— 兜底有效,只是触发时刻比修改前晚了"若干次 429 往返"的量级(秒级),可忽略。
|
修改后必须确认 stall 兜底没有被削弱:429 往返本身是生产性时间,不再计入 stall。
|
||||||
|
|
||||||
|
注意退避时长在纯 429 场景下**不随轮次增长**:429 免预算使 `fails` 恒为 0,`retry.py:243` 的 `max(fails, 1)` 令退避恒定在 `backoff_base_s` 档(或取 `Retry-After` 提示的较大值)。但这不影响结论——每轮的构成是「一次**快速失败**的 429 往返(网关立即拒绝,不耗 `timeout_s`,毫秒至秒级)」+「一段恒定退避 sleep(`backoff_base_s` 量级)」,后者是非生产性且**每轮都在累加**。故饱和期内非生产性时间仍占绝对多数,`stalled_s` 单调逼近 `stall_window_s`,兜底有效;触发时刻仅比修改前晚了"累计 429 往返耗时"的量级,可忽略。
|
||||||
|
|
||||||
## 4. 旧版行为审计(stall 子系统逐条)
|
## 4. 旧版行为审计(stall 子系统逐条)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user