docs: fold the Codex review into the issue #7 design and register it
Pins the ARCHITECTURE section 6.1 revision to land before or with the implementation, since the new scope reason contradicts the current single source of truth. Documents why SourceNotConfiguredError may sit outside the four-way classification: that rule governs transport-translated call failures, and the GatewayUnavailableError family already lives outside it. Also collapses the ten per-field response ternaries in emit_attempt into an _AttemptUsage view. They all expressed the same decision and pushed the method to cyclomatic complexity C, which blocked the commit gate.
This commit is contained in:
@@ -68,6 +68,8 @@ Issue 建议取 0。**否决**:下游 `schedule_retry(after_s=0)` 会立刻重
|
||||
|
||||
**选定**:`errors.py` 模块级常量 `GOVERNANCE_BACKEND_RETRY_AFTER_S = 5.0`,作为构造默认值,docstring 写明理由——后端恢复时间物理上不可知(不同于熔断冷却有确定到期时刻),取一个保守固定值;下游若有自己的退避策略可忽略此值。本库对 scope 级异常硬编码语义值已有先例(`retry.py:206` 的 `no_sources` 取 `0.0`)。
|
||||
|
||||
它**不是环境配置项**,故不落 CLAUDE.md §4.5「严禁硬编码默认值」的论域——§4.5 约束的是 `pydantic-settings` + `.env` 管辖的工程配置(超时、并发、限额),而本常量是异常自身携带的语义默认值,与 `no_sources` 取 `0.0` 同性质。docstring 需显式写明这一点,避免后来者误加环境键。
|
||||
|
||||
### 3.3 `scope` 的三层来源
|
||||
|
||||
| 层 | 构造点数 | scope 来源 | 改动 |
|
||||
@@ -122,7 +124,9 @@ Issue 建议取 0。**否决**:下游 `schedule_retry(after_s=0)` 会立刻重
|
||||
|
||||
## 7. 错误处理与测试策略
|
||||
|
||||
新失败面只有一个:`SourceNotConfiguredError`,落在四分类之外(它是装配缺陷而非调用失败),这是有意的——四分类描述"一次调用如何失败",装配缺陷不属于该论域。
|
||||
新失败面只有一个:`SourceNotConfiguredError`,它落在四分类之外。这**不违反** CLAUDE.md §4.2「一切失败必须落入四分类」——该铁律的论域是 **transport 层翻译的调用失败**(`ARCHITECTURE.md` §6.2 的翻译规则表逐条对应 HTTP 状态码与解析失败),而本库已有一整族异常合法地处在四分类之外:`GatewayUnavailableError` / `CircuitOpenError` / `AllSourcesExhausted` 都不是四分类之一,`ARCHITECTURE.md` §6.1 把它们单列一行,因为它们回答的是另一个问题——"整个 scope 还能不能用",而非"这一次调用怎么失败的"。
|
||||
|
||||
`SourceNotConfiguredError` 属于第三个论域:**装配缺陷**(配置与治理循环不一致,正常不可达)。四分类决定重试/换源/熔断,而装配缺陷根本不该进入治理循环去被"决定",它应当立刻失败并让人看见。将其塞进四分类中的任何一类都会赋予它一份不该有的治理语义(如 `RequestRejectedError` 会让下游以为请求本身有问题、去修请求)。§9 Q1 保留了"复用 `RequestRejectedError`"作为备选供人类权衡。
|
||||
|
||||
| 测试 | 位置 | 先失败后通过的证据 |
|
||||
|---|---|---|
|
||||
@@ -142,18 +146,34 @@ Issue 建议取 0。**否决**:下游 `schedule_retry(after_s=0)` 会立刻重
|
||||
| **版本** | **1.1.0**。有行为变更(下游对后端故障的处置路线改变)但无 API 破坏(加父类是扩大),按语义化版本走 minor |
|
||||
| **下游** | CHSAnalyzer3 当前在 1.0.1。升级后 `except GatewayUnavailableError` 即覆盖后端故障,其现有 `except GovernanceBackendError`(若有)继续有效,无需改代码即可获得修复 |
|
||||
|
||||
### 8.1 执行顺序(单一事实源纪律)
|
||||
|
||||
`ARCHITECTURE.md` 是架构单一事实源,`SCOPE_REASONS` 新增值域与 `GovernanceBackendError` 的归位都与其 §6.1 现状冲突。因此 **§6.1 的修订必须先于或同批于代码实现落地**,不得"先改代码、事后补文档"。具体为:人类批准本设计后,`writing-plans` 的第一项任务即为修订 `ARCHITECTURE.md` §6.1(补 `GovernanceBackendError` 与 `SourceNotConfiguredError` 行、scope 级 reason 值域增 `governance_backend_down`、记录本次归位的理由与日期),与实现同一分支、同批提交。
|
||||
|
||||
## 9. 待人类确认的决策点
|
||||
|
||||
| # | 决策 | 本文的选择 | 若你倾向不同 |
|
||||
(编号用 Q 前缀,避免与 `ARCHITECTURE.md` 的架构决策 D1–D14 混淆)
|
||||
|
||||
| # | 决策 | 本文的选择 | 备选 |
|
||||
|---|---|---|---|
|
||||
| D1 | "未知源"归到哪 | 拆为 `SourceNotConfiguredError`,**不**在 `GatewayUnavailableError` 之下(§3.4) | 若认为它该沿用 `GovernanceBackendError`,则 §3.4 整节作废,但需接受"配置写错永不进死信" |
|
||||
| D2 | `retry_after_s` 取值 | 常量 `5.0`(§3.2) | 可改为配置项或其他常量值;不建议取 0 |
|
||||
| D3 | 新类是否公共导出 | 是(进 `__init__.py`) | 若只作内部诊断可不导出 |
|
||||
| Q1 | "未知源"归到哪 | 拆为 `SourceNotConfiguredError`,**不**在 `GatewayUnavailableError` 之下(§3.4) | ① 沿用 `GovernanceBackendError`——需接受"配置写错永不进死信";② 复用 `RequestRejectedError`——留在四分类内、治理行为(不重试不换源快速失败)恰好正确,但语义错位会引下游去修请求 |
|
||||
| Q2 | `retry_after_s` 取值 | 常量 `5.0`(§3.2) | 可改为配置项或其他常量值;不建议取 0 |
|
||||
| Q3 | 新类是否公共导出 | 是(进 `__init__.py`) | 若只作内部诊断可不导出 |
|
||||
|
||||
## 10. 审批记录
|
||||
|
||||
| 阶段 | 状态 |
|
||||
|---|---|
|
||||
| Claude 自审 | 已完成(全部结论对应本会话内 grep/read 输出) |
|
||||
| Codex 独立审 | 待执行 |
|
||||
| Claude 自审 | 已完成(全部结论对应本会话内 grep/read 输出;§3.5 的 `self.args` 保全机制经 conda 环境实跑验证) |
|
||||
| Codex 独立审 | 已完成(2026-08-06),4 条意见逐条核验见下 |
|
||||
| 人类审批 | **待执行**(强制档:变更 `errors.py` 公共错误类型树) |
|
||||
|
||||
### 10.1 Codex 意见的核验结果
|
||||
|
||||
| 意见 | 判定 | 处置 |
|
||||
|---|---|---|
|
||||
| ARCHITECTURE §6.1 未同步前实施违反单一事实源(判为阻塞) | **实质成立**,但性质是执行顺序而非设计缺陷——§8 本已把 §6.1 回补列入影响面 | 新增 §8.1 明确"架构文档修订先于/同批于实现" |
|
||||
| §6.1 错误分类表未承认 `GovernanceBackendError`(判为阻塞) | **与上条同源**,且 §1.1 已自陈此为根因 | 同上,由 §8.1 覆盖 |
|
||||
| `SourceNotConfiguredError` 落在四分类外违反 §4.2 铁律(判为阻塞) | **部分成立**:铁律论域被误读——`GatewayUnavailableError` 族本就合法处在四分类之外(§6.1 单列一行)。但原文表述确会引起该疑虑 | §7 补写三个论域的划分论证;§9 Q1 增列"复用 `RequestRejectedError`"备选交人类权衡 |
|
||||
| 硬编码常量与 §4.5 存在张力(建议性) | **成立** | §3.2 补写"非环境配置项"及 docstring 要求 |
|
||||
| Q 编号与架构 D1–D14 混淆(建议性) | **成立** | §9 决策点编号由 `D` 改为 `Q` |
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
type: design
|
||||
node_id: design:governance-backend-error
|
||||
title: "治理后端故障归位为 scope 级不可用(Issue #7)"
|
||||
date: 2026-08-06
|
||||
---
|
||||
|
||||
# 治理后端故障归位为 scope 级不可用(Issue #7)
|
||||
|
||||
全文见 `2026-08-06-governance-backend-error-design.md`。来源: Gitea Issue #7(下游 CHSAnalyzer3 按异常类型分流失败)。**状态: 待人类审批。**
|
||||
|
||||
问题: 限流/熔断状态后端故障时库 fail-closed,一个请求都发不出去——语义上就是 scope 级不可用,但 `GovernanceBackendError` 是 `PolyGatewayError` 的**直接子类**,只写 `except GatewayUnavailableError` 的调用方接不住,于是 Redis 抖一下,积压任务一批批消耗业务失败预算进死信,而那是运维重启就好的故障。
|
||||
|
||||
## 选定方案
|
||||
|
||||
| 决策 | 选定 | 关键理由 |
|
||||
|---|---|---|
|
||||
| A 类型树 | `GovernanceBackendError` 改继承 `GatewayUnavailableError`,`SCOPE_REASONS` 增 `governance_backend_down`,`reason` 恒为该值 | 加父类是**扩大**不是破坏(既有 `except GovernanceBackendError` 照旧命中);库内仅 `telemetry.py:210` 一处捕父类且已并列写两者,**零回归** |
|
||||
| B `retry_after_s` | 模块常量 `GOVERNANCE_BACKEND_RETRY_AFTER_S = 5.0`,非环境配置项 | 后端恢复时间物理上不可知(不同于熔断冷却有确定到期时刻);取 0 会让积压任务零延迟批量重投,把一次故障放大成风暴 |
|
||||
| C scope 来源 | 后端层用 `self._scope`;`QuotaGate`/`BreakerGate` 构造函数注入,三处装配(`retry.py`/`ocr.py`/`embedding.py`)各传一行 | 两个包装器是后端异常的唯一入口,注入点收敛;三处装配本就持有 `self._scope` |
|
||||
| D 未知源拆分 | `_cfg()` 的 2 处改抛新增的 `SourceNotConfiguredError`,**有意不放在** `GatewayUnavailableError` 之下 | 那是装配缺陷不是后端故障;随整类归入"可重投"会让配置写错的任务永远重投、永不进死信——本 issue 要修的 bug 的镜像 |
|
||||
| E message 保全 | `super().__init__()` 后覆写 `self.args = (message,)` | 父类会把 message 覆盖为 `f"{scope} 网关暂时不可用: {reason}"`,而 22 处构造点的诊断串是排障主线索。机制已实跑验证 |
|
||||
|
||||
## 被否决的备选
|
||||
|
||||
| 备选 | 否决原因 |
|
||||
|---|---|
|
||||
| B(issue 原议): 只补文档,类型树不动 | 正确性依赖每个下游都读到那句话;本 issue 本身就是"文档读不出来"引发的,同一失效模式不能用同一种药治 |
|
||||
| C: 在 RetryMW 边界包成 `AllSourcesExhausted` | 比选定方案更具破坏性——下游现有 `except GovernanceBackendError` 直接失效 |
|
||||
| D: 后端层不再构造该异常,原始异常穿透由包装器统一翻译 | 初评时倾向。`redis/limiter.py:133,151` 的 `RedisPermit.release/settle` 依赖 `except GovernanceBackendError` 实现**释放侧降级**,穿透后接不住会破坏该既有行为;改 `except Exception` 则违反 P5 |
|
||||
| `retry_after_s` 复用 `BackpressureConfig.poll_interval_s` | 该值只有三个装配点持有,为此给后端加构造参数等于让状态存储层持有重投策略,违反 P7 |
|
||||
| 新增配置项 `PGW_GOVERNANCE_BACKEND_RETRY_AFTER_S` | YAGNI;无下游表达过需要,真需要时下游可忽略该字段用自有退避 |
|
||||
|
||||
## 对 issue 前提的四处修正
|
||||
|
||||
泄漏路径是**三条**不是两条(`retry.py:216` 的 `progress_age_s()` 同样在 catch 之外);构造点 **22 处**;其中 2 处语义完全不同(未知源);`retry_after_s=0` 语义通但工程不通。
|
||||
|
||||
根因记录: `ARCHITECTURE.md` §6.1 错误分类表里 `GovernanceBackendError` **一次都没出现**——它是 M2 引入分布式后端时新增的,当时未回补架构表,于是它在"调用方视角的分类学"中从来没有位置,README 的遗漏是这个遗漏的下游后果。
|
||||
|
||||
## 独立审查修正(2026-08-06, Codex)
|
||||
|
||||
4 条意见逐条核验: 两条"架构文档未同步"实质成立但性质是执行顺序 → 新增 §8.1 钉死"`ARCHITECTURE.md` §6.1 修订先于/同批于实现";"新错误类违反四分类铁律"**部分成立**——铁律论域被误读(`GatewayUnavailableError` 族本就合法处在四分类之外),但原表述确会引起疑虑 → §7 补写三论域划分论证,并把"复用 `RequestRejectedError`"增列为待人类权衡的备选;两条建议性意见(常量非配置项的说明、决策编号 `D`→`Q` 防与架构 D1–D14 混淆)已采纳。
|
||||
|
||||
相关: [[m2-distributed]]、[[m1-core-design]]、[[m25-resilience]]
|
||||
Reference in New Issue
Block a user