Files
PolyGateway/research-wiki/plans/2026-07-21-m25-resilience-plan.md

80 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M2.5 治理韧性实现计划
- **目标**: 按已批准设计 `designs/2026-07-21-m25-resilience-design.md`(方案 B 三件套)实现半死源隔离与健康感知调度,P6 原场景成功率 ≥98%。
- **方案概述**: 熔断加失败率通道(双 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`(已建,设计已提交)。执行者须先通读设计文档全文——本计划不重复设计论证,只给任务分解;**冲突时以设计文档为准**。
- **保真校验**: 本计划触及 M2 移植的熔断 Lua(蓝本 CHS scripts.py)。铁则: §T2 只允许**新增**域与判据分支,五操作的既有语义(epoch 仅开断+1、探针租约、release_probe 置 open_until=now、fencing 判据)逐字不动;每个 Lua 改动完成后逐段比对 M2 版本,确认既有行为无漂移(契约旧用例全绿即证据)。
## 文件结构(改动全景)
| 文件 | 动作 |
|---|---|
| `src/polygateway/types.py` | BreakerConfig 增 4 字段(frozen 只增): `min_calls:int=10` `fail_rate:float=0.6` `window_s:float=60.0` `max_cooldown_s:float` |
| `src/polygateway/ports.py` | 新增 `@runtime_checkable OutcomeAwareSelector` Protocol(仅 `record_outcome(source_name:str, ok:bool)->None`) |
| `src/polygateway/config.py` | `_load_breaker`: 全局并发抬升改源级;读 4 个新可选键 `{SCOPE}__BREAKER__{MIN_CALLS,FAIL_RATE,WINDOW_S,MAX_COOLDOWN_S}`(缺省 10/0.6/60/max(300,cooldown));`_SELECTORS``health_aware` 且缺省值改之 |
| `src/polygateway/sources.py` | 新增 `HealthAwareSelector`(EWMA α=0.2 初始 1.0、score=max(ewma,0.05)/(1+inflight)、P2C 头名、rng 注入) |
| `src/polygateway/backends/memory/breaker.py` | 双桶窗口 + 率通道 + reopen_streak/closed_since + 429 旁路 |
| `src/polygateway/backends/redis/breaker.py` | 同上,Lua 内实现(gate HASH 固定域 win_id/a0/f0/a1/f1) |
| `src/polygateway/middleware/breaker.py` | BreakerGate.record_failure 透传 reason(现签名若已含 reason 则不动,核对后定) |
| `src/polygateway/middleware/retry.py` | attempt_fails 局部变量传参、降权重排、record_outcome 喂数(isinstance 缓存) |
| `src/polygateway/client.py` | 装配 health_aware 缺省 + selector 注入 rng |
| `.env.example` | 阈值抬升注释改源级;新增 4 键注释与 SELECTOR=health_aware |
| `research-wiki/migrations/chsanalyzer.md` | 标注: retry_after 上限 300s(arq defer 放大)、双通道偏离、缺省选源变更 |
| `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` 真实等待变体补齐——新时间用例照此双轨) |
## 任务清单(每任务 = 一次提交,先红后绿)
### T1 类型与配置层
- [ ] `types.py` BreakerConfig 增 4 字段;`config.py` `_load_breaker` 改源级抬升 + 4 新键(缺省注释写明依据"韧性参数缺省先例");`_SELECTORS` 增 health_aware 并设缺省。
- 测试(先红): `tests/unit/test_config.py`(实际文件名以 grep 为准)——① 无新键时缺省 10/0.6/60/max(300,cooldown);② 源级抬升: 源 max_concurrency=8 → 阈值 max(配置,16),**无源级并发时不抬升**(全局并发 100 不再影响);③ SELECTOR 缺省 health_aware、显式 round_robin 仍可选。
- 验证: `conda run -n PolyGateway pytest tests/unit/test_config*.py -q` PASS。
### T2 熔断双后端(核心,契约驱动)
- [ ] 先在 `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")两通道完全旁路;`record_success(entry, *, count_attempt=True)`——True 时窗口 attempts+1,False(ResultInvalid/健康拒绝)完全不动窗口。
- [ ] 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 版本。
- 验证: `conda run -n PolyGateway pytest tests/contracts -q` 全绿(memory+redis 双参数,旧用例零破坏=保真门)+ `pytest -m slow tests/integration/test_redis_governance_time.py -q` 绿。
### T3 健康选源器
- [ ] `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 分支)。
- 验证: `pytest tests/unit/test_health_selector.py -q` PASS。
### 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_attempt=False;embedding.py 两调用点同步(文件表口径)。**record_outcome 调用必须 try/except 吞并 warning**(设计 §4: 选源器异常不得打断真实成功返回路径),含对应单测。
- 测试(先红): 既有 `tests/unit/test_retry.py` 追加——① 单失败源仍首选、双失败移尾;② 并发两调用互不串 attempt_fails;③ record_outcome 喂数矩阵(五出口各断言)+ 抛异常不影响返回;④ reasons/异常语义回归(旧用例全绿即可)。
- 验证: `pytest tests/unit -k retry -q` PASS;`make ci` 全绿。
### T5 文档与迁移标注
- [ ] `.env.example`(阈值注释改源级 + 4 新键 + SELECTOR)、`migrations/chsanalyzer.md` 标注四项: ① retry_after 上限 300s(G1 arq defer 放大)② 缺省选源变更 ③ 全局抬升废除改源级 ④ 双通道偏离;ARCHITECTURE.md 熔断节补双通道一段 + "治理粒度=配额粒度"原则一句(设计 §3.4)。
- 验证: `make lint` 绿;grep 确认 .env.example 无旧"并发×2"全局表述。
### T6 验收: P6 原场景回归(硬门)
- [ ] `make ci` 全绿后,以与首跑**逐字节相同**的命令重跑 P6(tmux,`--run-id` 留默认新 id): `PGW_PRICING_PATH=config/prices.json PGW_LIMITER_BACKEND=redis PGW_BREAKER_BACKEND=redis PGW_CACHE_BACKEND=redis PGW_CACHE_NAMESPACE=soak PGW_CACHE_TTL_S=21600 conda run -n PolyGateway --no-capture-output python -u tools/soak/run_soak.py --scenario P6 --budget-calls 8000 --budget-tokens 200000000 --workers 2 --concurrency 32 --scope SOAK --max-hours 3`。**.env 一字不改**。
- [ ] 缓存命中会抬高表观成功率——验收口径按**总成功率 ≥98%**(用户原话口径),同时报告"排除缓存命中后的真实调用成功率"作诚实附注;八不变量全 PASS;对照首跑出对比表(成功率/尝试流向/源5 吸量/429 量/成本)。
- [ ] 不达标: 按遥测数据定位短板(预期迭代旋钮: fail_rate、EWMA α、地板值、min_calls),每轮改动过契约测试后重跑;用户已授权多轮。
- [ ] 达标后: findings 记 M2.5 验收 + ROADMAP 补行 + 独立 verifier(全新上下文)核验全分支 → 清零后合并 main(用户已豁免过目)。
## 关键接口(跨任务消费,写死于此)
```python
# ports.py 新增(完整)
@runtime_checkable
class OutcomeAwareSelector(Protocol):
def record_outcome(self, source_name: str, ok: bool) -> None: ...
# ProviderGate.record_success 签名演进(唯一端口触碰,关键字参数带默认值)
async def record_success(self, entry: GateDecision, *, count_attempt: bool = True) -> GateUpdate: ...
# HealthAwareSelector 构造
def __init__(self, *, rng: Callable[[], float] = random.random) -> None: ...
```
BreakerConfig 新字段名: `min_calls` `fail_rate` `window_s` `max_cooldown_s`(env 键 `{SCOPE}__BREAKER__MIN_CALLS` 等四个)。gate HASH 新域名: `win_id a0 f0 a1 f1 reopen_streak closed_since`
## 风险与回滚
- 每任务独立提交,T2 是最大风险面(Lua)——契约旧用例全绿是保真门,任何旧用例破坏立即停下按 systematic-debugging 根因。
- P6 重跑约 1-2 小时、成本约 25 元/轮(已有价格表),预算帽/守卫机制原样生效。