docs: revise M2.5 plan per independent review

This commit is contained in:
2026-07-21 08:41:17 -04:00
parent 3542917ba8
commit 8e2673487a
@@ -1,7 +1,7 @@
# M2.5 治理韧性实现计划 # M2.5 治理韧性实现计划
- **目标**: 按已批准设计 `designs/2026-07-21-m25-resilience-design.md`(方案 B 三件套)实现半死源隔离与健康感知调度,P6 原场景成功率 ≥98%。 - **目标**: 按已批准设计 `designs/2026-07-21-m25-resilience-design.md`(方案 B 三件套)实现半死源隔离与健康感知调度,P6 原场景成功率 ≥98%。
- **方案概述**: 熔断加失败率通道(双 30s 桶,429 不入)+ 开路指数退避;新增 HealthAwareSelector(EWMA×在途,0.05 地板探索)设为缺省;RetryMW 调用内失败降权。全部进程本地或并入既有 Redis gate HASH,零新依赖、零端口签名变更(ports.py 增 OutcomeAwareSelector) - **方案概述**: 熔断加失败率通道(双 30s 桶,429 不入)+ 开路指数退避;新增 HealthAwareSelector(EWMA×在途,0.05 地板探索)设为缺省;RetryMW 调用内失败降权。全部进程本地或并入既有 Redis gate HASH,零新依赖。**端口面变更两处(如实声明)**: ports.py 增 OutcomeAwareSelector;`ProviderGate.record_success` 增关键字参数 `count_attempt: bool = True`——波及点全清单: ports.py:155、backends/memory/breaker.py:134、backends/redis/breaker.py:280、middleware/breaker.py:28(BreakerGate 包装)、middleware/retry.py 三处(:244 真实成功 True、:255 ResultInvalid False、:281 健康拒绝 False)、**embedding.py 两处**(:272 真实成功 True、:306 `_gate_on_terminal` ResultInvalid/健康拒绝 False)。`record_failure` 现签名已含 reason 透传(middleware/breaker.py:36),**无需改动**
- **分支**: `feature/m25-resilience`(已建,设计已提交)。执行者须先通读设计文档全文——本计划不重复设计论证,只给任务分解;**冲突时以设计文档为准**。 - **分支**: `feature/m25-resilience`(已建,设计已提交)。执行者须先通读设计文档全文——本计划不重复设计论证,只给任务分解;**冲突时以设计文档为准**。
- **保真校验**: 本计划触及 M2 移植的熔断 Lua(蓝本 CHS scripts.py)。铁则: §T2 只允许**新增**域与判据分支,五操作的既有语义(epoch 仅开断+1、探针租约、release_probe 置 open_until=now、fencing 判据)逐字不动;每个 Lua 改动完成后逐段比对 M2 版本,确认既有行为无漂移(契约旧用例全绿即证据)。 - **保真校验**: 本计划触及 M2 移植的熔断 Lua(蓝本 CHS scripts.py)。铁则: §T2 只允许**新增**域与判据分支,五操作的既有语义(epoch 仅开断+1、探针租约、release_probe 置 open_until=now、fencing 判据)逐字不动;每个 Lua 改动完成后逐段比对 M2 版本,确认既有行为无漂移(契约旧用例全绿即证据)。
@@ -20,7 +20,8 @@
| `src/polygateway/client.py` | 装配 health_aware 缺省 + selector 注入 rng | | `src/polygateway/client.py` | 装配 health_aware 缺省 + selector 注入 rng |
| `.env.example` | 阈值抬升注释改源级;新增 4 键注释与 SELECTOR=health_aware | | `.env.example` | 阈值抬升注释改源级;新增 4 键注释与 SELECTOR=health_aware |
| `research-wiki/migrations/chsanalyzer.md` | 标注: retry_after 上限 300s(arq defer 放大)、双通道偏离、缺省选源变更 | | `research-wiki/migrations/chsanalyzer.md` | 标注: retry_after 上限 300s(arq defer 放大)、双通道偏离、缺省选源变更 |
| 测试 | 见各任务;契约测试改动落 `tests/unit/test_breaker_contract.py`(先核实实际契约文件名,沿用既有参数化机制)与 `tests/integration/test_redis_governance_time.py` slow 变体 | | `src/polygateway/embedding.py` | record_success 两调用点补 count_attempt(:272 True、:306 False);embedding 独立选源循环**不装配 health_aware、不喂 record_outcome**(其源池独立且流量小,沿用既有选源;设计范围仅 chat 主循环——口径写死于此) |
| 测试 | 契约测试 = `tests/contracts/test_breaker_contract.py`(conftest `backend` fixture 参数化 memory+redis,redis 时间用例 SkipClock 跳过、由 `tests/integration/test_redis_governance_time.py` 真实等待变体补齐——新时间用例照此双轨) |
## 任务清单(每任务 = 一次提交,先红后绿) ## 任务清单(每任务 = 一次提交,先红后绿)
@@ -30,23 +31,23 @@
- 验证: `conda run -n PolyGateway pytest tests/unit/test_config*.py -q` PASS。 - 验证: `conda run -n PolyGateway pytest tests/unit/test_config*.py -q` PASS。
### T2 熔断双后端(核心,契约驱动) ### T2 熔断双后端(核心,契约驱动)
- [ ] 先在**契约测试套件**加设计 §5 七组用例(注入确定性序列;时间推进用既有 clock 机制,redis 变体真实等待 + 小 MAX_COOLDOWN_S)→ 两后端同时红 - [ ] 先在 `tests/contracts/test_breaker_contract.py` 加设计 §5 七组用例(①高失败率序列 min_calls 处开路 ②样本<min_calls 不率开 ③连续阈值=配置且源级抬升 ④退避 2^n 封顶+衰减 ⑤ResultInvalid/健康拒绝 attempts/failures 均不入 ⑥429 脉冲不开路、timeout 脉冲开路且不增 streak ⑦率开路 failure_count 顶格;全部注入确定性序列)→ 两后端同时红;时间推进类用例同步落 `tests/integration/test_redis_governance_time.py` 真实等待变体(用小 MAX_COOLDOWN_S 配置控时长)
- [ ] memory/breaker.py 实现: 状态机域清单表(设计 §3.1)逐域实现;429(reason=="rate_limited")完全旁路;ResultInvalid/健康拒绝走 record_success 但**带 count_attempt=False 语义**(实现口径: record_success 增 attempts 仅当真实成功——需在 BreakerGate 调用点区分,新增参数 `count:bool=True`,ResultInvalid/健康拒绝处传 False;端口 Protocol 方法签名如需动,只加带默认值的关键字参数,旧实现兼容) - [ ] memory/breaker.py: 状态机域清单表(设计 §3.1)逐域实现;429(reason=="rate_limited")两通道完全旁路;`record_success(entry, *, count_attempt=True)`——True 时窗口 attempts+1,False(ResultInvalid/健康拒绝)完全不动窗口
- [ ] redis/breaker.py Lua: RECORD_FAILURE 增窗口 HINCRBY+率判据+streak;RECORD_SUCCESS 增 attempts 计数与 streak 衰减(closed_since 比较),**注意现有 HSET 整表重置不得清新域**;开路分支统一算 cooldown_eff。逐段对照保真 - [ ] redis/breaker.py Lua: RECORD_FAILURE 增窗口固定域轮换(win_id/a0/f0/a1/f1)+率判据+streak;RECORD_SUCCESS 增 attempts 计数与 streak 衰减(closed_since 比较);开路分支统一算 cooldown_eff。**保真注意**: 现有 RECORD_SUCCESS 用 HSET 固定域列表(不删未列域),风险在显式清零清错域——只清 `failures`,新域按域表管理;逐段对照 M2 版本
- 验证: `pytest tests/unit -k breaker -q` `pytest tests/integration -k "redis and (breaker or governance)" -q` 全绿;`pytest -m slow -k breaker` 真实等待变体绿(耗时可控) - 验证: `conda run -n PolyGateway pytest tests/contracts -q` 全绿(memory+redis 双参数,旧用例零破坏=保真门)+ `pytest -m slow tests/integration/test_redis_governance_time.py -q` 绿
### T3 健康选源器 ### T3 健康选源器
- [ ] `ports.py` OutcomeAwareSelector;`sources.py` HealthAwareSelector(rng 注入构造参数,P2C 用 rng 取两索引);`client.py` 装配。 - [ ] `ports.py` OutcomeAwareSelector;`sources.py` HealthAwareSelector(rng 注入构造参数,P2C 用 rng 取两索引);`client.py` 装配: `_build_selector`(:275)增 rng 形参、`from_settings`(:204)把 GatewayClient 既有 `rng`(:81)传入;构造函数全量注入路径由调用方自备 selector,不变
- 测试(先红): 新文件 `tests/unit/test_health_selector.py`——① 全 1.0 初始时近似均匀(注入定值 rng 断言确定性结果);② 喂 10 次失败后 score 地板 0.05 仍偶被选(rng 定向构造);③ 恢复源连续成功 EWMA 爬升(0.2→…);④ inflight 抑制;⑤ 非 OutcomeAware 选源器在 RetryMW 中零调用(isinstance 分支)。 - 测试(先红): 新文件 `tests/unit/test_health_selector.py`——① 全 1.0 初始时近似均匀(注入定值 rng 断言确定性结果);② 喂 10 次失败后 score 地板 0.05 仍偶被选(rng 定向构造);③ 恢复源连续成功 EWMA 爬升(0.2→…);④ inflight 抑制;⑤ 非 OutcomeAware 选源器在 RetryMW 中零调用(isinstance 分支)。
- 验证: `pytest tests/unit/test_health_selector.py -q` PASS。 - 验证: `pytest tests/unit/test_health_selector.py -q` PASS。
### T4 RetryMW 编排 ### T4 RetryMW 编排
- [ ] attempt_fails 局部变量 + `_pick_runnable(reasons, attempt_fails)` 降权重排(≥2 次失败移尾,全失败回退原序);`_attempt` 出口喂 record_outcome(真实成功 ok=True;Transient/SourceDead/429 ok=False;ResultInvalid/健康拒绝不喂);ResultInvalid/健康拒绝处 record_success 传 count=False - [ ] attempt_fails 局部变量 + `_pick_runnable(reasons, attempt_fails)` 降权重排(≥2 次失败移尾,全失败回退原序);`_attempt` 出口喂 record_outcome(真实成功 ok=True;Transient/SourceDead/429 ok=False;ResultInvalid/健康拒绝不喂);ResultInvalid/健康拒绝处 record_success 传 count_attempt=False;embedding.py 两调用点同步(文件表口径)。**record_outcome 调用必须 try/except 吞并 warning**(设计 §4: 选源器异常不得打断真实成功返回路径),含对应单测
- 测试(先红): 既有 `tests/unit/test_retry*.py` 追加——① 单失败源仍首选、双失败移尾;② 并发两调用互不串 attempt_fails;③ record_outcome 喂数矩阵(五出口各断言);④ reasons/异常语义回归(旧用例全绿即可)。 - 测试(先红): 既有 `tests/unit/test_retry.py` 追加——① 单失败源仍首选、双失败移尾;② 并发两调用互不串 attempt_fails;③ record_outcome 喂数矩阵(五出口各断言)+ 抛异常不影响返回;④ reasons/异常语义回归(旧用例全绿即可)。
- 验证: `pytest tests/unit -k retry -q` PASS;`make ci` 全绿。 - 验证: `pytest tests/unit -k retry -q` PASS;`make ci` 全绿。
### T5 文档与迁移标注 ### T5 文档与迁移标注
- [ ] `.env.example``migrations/chsanalyzer.md`(§6 三行审计表内容)、ARCHITECTURE.md 若 §7.4 熔断节有连续失败表述则补双通道一段(引设计文档,不展开)。 - [ ] `.env.example`(阈值注释改源级 + 4 新键 + SELECTOR)`migrations/chsanalyzer.md` 标注四项: ① retry_after 上限 300s(G1 arq defer 放大)② 缺省选源变更 ③ 全局抬升废除改源级 ④ 双通道偏离;ARCHITECTURE.md 熔断节补双通道一段 + "治理粒度=配额粒度"原则一句(设计 §3.4)。
- 验证: `make lint` 绿;grep 确认 .env.example 无旧"并发×2"全局表述。 - 验证: `make lint` 绿;grep 确认 .env.example 无旧"并发×2"全局表述。
### T6 验收: P6 原场景回归(硬门) ### T6 验收: P6 原场景回归(硬门)