错误分类:GovernanceBackendError 不在 GatewayUnavailableError 之下,且公开错误面没有文档 #7
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
调用方(CHSAnalyzer3)要按异常类型把外呼失败分成几类处置,其中一类是「整个用途暂时
不可用 → 延期重投、不消耗业务失败预算」。自然的写法是
except GatewayUnavailableError,因为它的 docstring 就写着「业务侧 catch 本类做延期重投(CHS arq 模式)」。
按 1.0.1 的源码核了一遍,有两件事想提。
一、
GovernanceBackendError语义上属于 scope 级不可用,类型上却不在GatewayUnavailableError之下它的含义是限流/熔断的状态后端自己坏了,而降级方向是 fail-closed——这时候一个请求都发不
出去,正是「整个 scope 暂时不可用」。但它是
PolyGatewayError的直接子类。它确实会到达调用方:记账那条路被
_record_quietly降级成 warning(
middleware/retry.py:404),但闸门那条路(try_enter/try_acquire,middleware/breaker.py:23、backends/redis/limiter.py:250)没有这层包裹,会一路抛出去。于是只写
except GatewayUnavailableError的调用方会把它落进兜底那一类。对我们这边的后果很具体:Redis 抖一下,积压的任务一批批消耗失败预算,够到上限就进死信——而那是个
运维重启一下就好的故障。
看得出为什么它现在不是子类:
GatewayUnavailableError.__init__强制要求scope、reason、retry_after_s三项,而reason必须取自SCOPE_REASONS,后端故障不属于其中任何一个,
retry_after_s也没有一个算得出来的值。所以这不是写错了,是那个构造契约容不下这一种。
两个方向,倾向哪个由你们定:
SCOPE_REASONS加一个governance_backend_down,让GovernanceBackendError继承
GatewayUnavailableError,retry_after_s取 0——docstring 里已经写了「0 = 可立即重试」,语义上是通的。好处是调用方一条
except就覆盖完整。二、看不出哪些异常会到达调用方,哪些是库内吸收的
这一条其实比上一条更值得改,因为它已经让我们写错过一次东西。
TransientError和SourceDeadError的 docstring 描述的是治理行为(「退避后可重试、可换源、计熔断」「不重试,立即换源,该源 force_open」),读起来像是
调用方要处理的东西。实际上
middleware/retry.py:364那一行except (SourceDeadError, TransientError)把它们接住了、return _Failed(...),重试预算耗尽时统一包成
AllSourcesExhausted——它们根本到不了调用方。我们第一遍只看类型树和 docstring,据此在自己的设计文档里写了一整段「漏接
SourceDeadError会让一次凭据过期烧掉业务失败预算」,是核到retry.py才发现不成立、整段删掉的。
建议在
errors.py的模块 docstring 或 README 里加一张两列的表,明确区分:GatewayUnavailableError(含AllSourcesExhausted、CircuitOpenError)TransientErrorRequestRejectedErrorSourceDeadErrorResultInvalidErrorGovernanceBackendError(仅闸门路径)这样调用方不必读
middleware/retry.py才知道自己该 catch 什么。环境:polygateway 1.0.1(conda 环境
chs,Python 3.13)。上面每一处行号都是照
src/polygateway/当前代码数的。已在 1.1.0 落地并合入 main(
9c2824c),关闭。两条诉求都做了,方向按你给的 A(类型树归位)而非 B(只补文档)。一、
GovernanceBackendError归位4507348改为继承GatewayUnavailableError,reason恒为新增的governance_backend_down。你写的「A 与 B 倾向哪个由你们定」——选 A 的理由是 B 的正确性依赖每个下游都读到那句话,而本 issue 本身就是"文档读不出来"引发的,同一失效模式不能用同一种药治。retry_after_s没取 0,取了常量 5.0。 你说的「docstring 里已经写了 0 = 可立即重试,语义上是通的」语义确实通,但工程上不通: Redis 挂掉时积压任务会以零延迟批量重投,对着一个已经挂掉的后端打忙循环,把一次故障放大成一场风暴。常量定义在errors.py的GOVERNANCE_BACKEND_RETRY_AFTER_S,不是环境配置项(后端恢复时间物理上不可知,不同于熔断冷却有确定到期时刻);你们有自己的退避策略可以忽略这个字段。你的泄漏路径清单少了三条。 你列了
try_enter/try_acquire,实际是五条:QuotaGate的try_acquire/stats/progress_age_s,BreakerGate的try_enter/retry_after_s。判据是该 gate 调用点是否被_record_quietly包裹——未包裹即直达调用方。五条现在都携带scope/reason/retry_after_s;str(exc)仍是原诊断串(如限流后端 try_acquire 失败: ...),结构化字段与诊断信息并存。对 CHSAnalyzer3 的直接影响:
tracking.py一条except GatewayUnavailableError即覆盖完整,无需为后端故障单列分支。你们现有的except GovernanceBackendError(若有)继续有效——加父类是扩大捕获面,不是破坏。二、你没提但必须一起处理的一件事
限流后端
_cfg()在源名不在配置字典中时也抛GovernanceBackendError(redis/limiter.py:198、memory/limiter.py:92)。那不是后端故障,是装配缺陷。若随整类归入"可延期重投",配置写错的任务会永远重投、永不进死信、无人告警——正是本 issue 要修的 bug 的镜像。故新增
SourceNotConfiguredError(公共导出),有意不在GatewayUnavailableError之下。你们那边它会落进_TERMINAL分支消耗失败预算进死信,这是刻意的: 配置写错就该让人看见。三、公开错误面文档(你说这条比第一条更值得改)
README 新增两列表,明确
TransientError/SourceDeadError被middleware/retry.py:365接住、耗尽时统一包成AllSourcesExhausted,根本到不了调用方——你们据此写错整段设计文档那件事,现在读 README 就能避免,不必去读middleware/retry.py。ARCHITECTURE.md§6.1 也回补了这两个类(该类自 M2 引入分布式后端起就没进过那张表,这正是 README 遗漏的根因)。四、独立验证抓到的两个缺陷(都已修,值得你们知道)
a57a5ce: 首版实现里 gate 包装器的except GovernanceBackendError: raise只放行旧类型,SourceNotConfiguredError落进下一行except Exception被重新包回GovernanceBackendError——第二条在唯一的生产路径上等于没做。原测试没抓到,是因为它们直接打私有_cfg(),比生产路径低一层。5853c3f: 上条的修复让该异常能穿透_record_quietly,而那层降级的存在理由是"调用已真实完成,写回失败不该丢弃成功响应"。同批放宽了三处_record_quietly。升级注意
处置路线会变: 后端故障从"落进兜底分支、按业务失败处置"变为"按 scope 级不可用延期重投、不消耗失败预算"。这正是修复目标,但升级前请确认你们的兜底分支没有依赖旧行为(例如靠它触发告警)。完整说明见 CHANGELOG 1.1.0。
安装:
pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ "polygateway[redis,postgres,structured]==1.1.*"