-
1.2.4 Stable
released this
2026-08-20 16:01:11 +08:00 | 91 commits to main since this release熔断开路时,调用方第一次可以选择等而不是当场失败(issue #14)。此前准入侧有一格是空的:限流闸满时库允许排队(
{SCOPE}__QUOTA_FULL=wait|fail_fast,缺省wait),熔断门拒绝时只有 fail-fast 一档且不可配——而两者在准入语义上是同构的,都没发出请求、都带着"稍后再来"的提示。新键{SCOPE}__CIRCUIT_OPEN=fail_fast|wait补上这一格,形状与QUOTA_FULL逐项对齐。缺省是
fail_fast,即今天的行为,存量部署无需改动任何配置。要改的是单源 scope:熔断的设计前提是"这个源坏了,把流量导到别的源",只配了一个源时这个前提不成立,同一段代码做的事就变成"这个源坏了,所以整个 scope 停止服务"。提交方实测:中转抖动 36 秒(22 次尝试 / 19 次 503)触发失败率通道开路,随后 30 次调用全部在 7-74 毫秒内失败,MAX_ATTEMPTS=8一格没用上,一条跑了 3 小时 18 分钟的实验臂当场报废。配wait之后,熔断对配额和钱包的保护完整保留(等待期照样一个请求都不发),改变的只是调用方当场死还是排队等;代价是单次调用最坏墙钟被拉长——上限是STALL_WINDOW_S(缺省 300 秒)。但wait并不豁免重试预算: 冷却结束后放行的探针是一次真实尝试,失败照样烧一格MAX_ATTEMPTS,所以密钥失效(401/403)这类一击即熔的源通常更早以reason=retry_exhausted失败,而不是等满窗口后的stalled;两者哪个先到取决于MAX_ATTEMPTS与冷却时长、STALL_WINDOW_S的相对大小。库无法区分"密钥坏了"和"中转抖了",选wait就是声明"宁可等也不当场死"。请先读这一条:
retry_after_s在半开状态下的取值变了(缺省档同样生效)retry_after_s从来没有写下来的定义,于是两个后端各自发挥、互相漂移。现在它只回答一个问题:距离确定可再试的时刻还有多久。健康与准入允许 →0.0;开路 → 剩余冷却;半开(探针在途)→0.0,因为探针随时可能出结果,不存在确定的时刻——而0 = 可立即重试本就是这个字段的既有约定。变更点在半开:此前返回的是探针租约剩余。那是个死锁保护参数,派生自
max(2 × 最慢源 TIMEOUT_S, COOLDOWN_S, TIMEOUT_S + 5),与"这个源多久能恢复"没有任何因果关系。TIMEOUT_S=300的部署里它是 600 秒,而冷却期只有 60 秒。照它延期重投的下游,等的是一个物理上无意义的数。更重的后果在库内,提交方也没发现:这个值被写进了源冷却备忘,而备忘的
set_until取更晚者、不可回退。于是——源开路、冷却到期、调用①拿到探针、并发的调用②被拒并给该源记下 600 秒本地冷却、调用①的探针成功、门恢复 CLOSED——本进程此后仍然跳过这个健康的源将近 10 分钟。单源下每次调用照旧抛CircuitOpenError;多源部署同样中招,只是别的源接住了流量,池子越大越隐蔽。修正后备忘写进的是一个已经过期的时刻,自动回到"只记开路的确定冷却期"。同批统一了两个后端在六个出口上的口径。其中四处是既有的分叉:Redis 在授予探针时返回探针 TTL、在写回被 fencing 拒时返回租约剩余,而内存后端一直返回 0。契约测试此前只钉了"第二个进入者会被拒绝",从没钉过它拿到的是什么数,这个盲区把分叉掩护到了今天。
其他
_pick_runnable/_on_no_runnable此前在 chat/embedding/OCR 三条治理循环里各存一份逐字复制,现收敛为middleware/admission.py::SourceAdmission一份。行为不变——差异用注入表达(调用内降权传空计数时恒等、AIMD pacer 为None时跳过),permit结算的 warning 文案由三种归一为一种。GatewayUnavailableError的文档收回了重试职责:调用级的重试、退避、换源、等待冷却全部在库内,本异常表示那份预算已经用尽;下游据此再投属于任务级重试,语义不同。此前那句"业务侧 catch 本类做延期重投"读起来像在鼓励每个下游各写一份重试逻辑,而两边各写一份必然漂移。
Downloads