docs: add the implementation plan for issue #7

Five tasks, with the ARCHITECTURE section 6.1 revision first so the code
never contradicts the single source of truth, and the reparenting kept
atomic because scope is a required keyword argument and any split would
leave an unrunnable tree.

The Codex review caught that the planned test evidence pointed at the
wrong stubs: the ones at test_backpressure.py:176-186 cover accounting-side
degradation, not the three gate paths that actually leak to callers, and
try_acquire and try_enter have no stub at all.
This commit is contained in:
2026-08-06 04:11:17 -04:00
parent 3a104fcce4
commit 1fa91cf73d
7 changed files with 325 additions and 7 deletions
@@ -31,7 +31,7 @@
| 事实 | 证据 | 含义 |
|---|---|---|
| 库内仅一处 `except GatewayUnavailableError` | `middleware/telemetry.py:210`,写法为 `except (GatewayUnavailableError, GovernanceBackendError)` | 变成父子关系后该处由"并列捕获"退化为"父类捕获",**行为逐字不变**,库内零回归 |
| 库内仅一处 `except GatewayUnavailableError` | `middleware/telemetry.py:250`,写法为 `except (GatewayUnavailableError, GovernanceBackendError)` | 变成父子关系后该处由"并列捕获"退化为"父类捕获",**行为逐字不变**,库内零回归 |
| 加父类是纯扩大 | 下游既有 `except GovernanceBackendError` 全部照旧命中 | 不违反 CLAUDE.md §4.3「已被下游消费的公共类型只增不删不改名」 |
| `QuotaGate`/`BreakerGate` 是后端异常的唯一入口 | 两类 docstring 自述,三处装配 `retry.py:186` / `ocr.py:122` / `embedding.py:123` | scope 注入点收敛为 2 个类、3 处装配 |
| 三个装配点都持有 `self._scope` | `retry.py:183``ocr.py:116``embedding.py:118` | 注入无需新增上游参数传递链 |
@@ -108,7 +108,7 @@ Issue 建议取 0。**否决**:下游 `schedule_retry(after_s=0)` 会立刻重
| 限流/熔断后端不可用 → 报错而非放行(fail-closed) | 库铁律 | **保留**,一字不改 |
| 记账路径后端故障降级为 warning | `middleware/retry.py:404` `_record_quietly` | **保留**。仅闸门路径需要到达调用方 |
| permit `release`/`settle` 失败降级 warning | `redis/limiter.py:133,151` | **保留**(§4 路线 D 因此被否决) |
| 遥测对后端故障发 `emit_terminal_failure` | `middleware/telemetry.py:210` | **保留**,父子关系后由父类分支承接,行为不变 |
| 遥测对后端故障发 `emit_terminal_failure` | `middleware/telemetry.py:250` | **保留**,父子关系后由父类分支承接,行为不变 |
| `except GovernanceBackendError: raise` 原样放行 | 包装器 9 处 | **保留** |
| "未知源"抛 `GovernanceBackendError` | `memory/limiter.py:92``redis/limiter.py:198` | **替换**为 `SourceNotConfiguredError`(§3.4) |
| `str(exc)` 为诊断串 | 22 处 | **保留**(§3.5 显式保全) |
@@ -131,9 +131,9 @@ Issue 建议取 0。**否决**:下游 `schedule_retry(after_s=0)` 会立刻重
| 测试 | 位置 | 先失败后通过的证据 |
|---|---|---|
| `GovernanceBackendError` 可被 `except GatewayUnavailableError` 接住 | `tests/unit/test_errors.py` | 改前 `pytest.raises(GatewayUnavailableError)` 必失败 |
| 三条泄漏路径(`try_acquire`/`try_enter`/`progress_age_s`)抛出的异常携带正确 `scope` 与非零 `retry_after_s` | `tests/unit/test_backpressure.py`(已有 `:176-186` 的注入桩可复用) | 改前无 `scope` 属性,`AttributeError` |
| 三条泄漏路径(`try_acquire`/`try_enter`/`progress_age_s`)抛出的异常携带正确 `scope` 与非零 `retry_after_s` | `tests/unit/test_backpressure.py`**三条桩都需新增**(Codex 审计划时核出: `:176-186` 是记账侧 `record_success`/`record_failure`/`mark_progress` 的降级桩,不是闸门路径;`progress_age_s``:243-257` 覆盖包装行为、不验 scope) | 改前无 `scope` 属性,`AttributeError` |
| `str(exc)` 仍为原诊断串 | `tests/unit/test_errors.py` | 防 §3.5 回归 |
| 未知源抛 `SourceNotConfiguredError` 且**不是** `GatewayUnavailableError` | `tests/unit/test_redis_key_layout.py:71-73`内存版对应用例 | 改前抛 `GovernanceBackendError`,断言"不是 scope 级"必失败 |
| 未知源抛 `SourceNotConfiguredError` 且**不是** `GatewayUnavailableError` | `tests/unit/test_redis_key_layout.py:70-74`;内存版**当前无覆盖,需新增** | 改前抛 `GovernanceBackendError`,断言"不是 scope 级"必失败 |
| Redis 真实掉线时准入侧行为 | `tests/integration/test_redis_cross_connection.py:228-245`(真实 Redis,不 mock) | 断言由 `GovernanceBackendError` 收紧为"是 `GatewayUnavailableError``reason == governance_backend_down`" |
## 8. 影响面清单
@@ -15,7 +15,7 @@ date: 2026-08-06
| 决策 | 选定 | 关键理由 |
|---|---|---|
| A 类型树 | `GovernanceBackendError` 改继承 `GatewayUnavailableError`,`SCOPE_REASONS``governance_backend_down`,`reason` 恒为该值 | 加父类是**扩大**不是破坏(既有 `except GovernanceBackendError` 照旧命中);库内仅 `telemetry.py:210` 一处捕父类且已并列写两者,**零回归** |
| A 类型树 | `GovernanceBackendError` 改继承 `GatewayUnavailableError`,`SCOPE_REASONS``governance_backend_down`,`reason` 恒为该值 | 加父类是**扩大**不是破坏(既有 `except GovernanceBackendError` 照旧命中);库内仅 `telemetry.py:250` 一处捕父类且已并列写两者,**零回归** |
| 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 的镜像 |