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.
4.3 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| design | design:governance-backend-error | 治理后端故障归位为 scope 级不可用(Issue #7) | 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 混淆)已采纳。