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

79 lines
8.3 KiB
Markdown
Raw 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)。
- **分支**: `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 放大)、双通道偏离、缺省选源变更 |
| 测试 | 见各任务;契约测试改动落 `tests/unit/test_breaker_contract.py`(先核实实际契约文件名,沿用既有参数化机制)与 `tests/integration/test_redis_governance_time.py` slow 变体 |
## 任务清单(每任务 = 一次提交,先红后绿)
### 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 熔断双后端(核心,契约驱动)
- [ ] 先在**契约测试套件**加设计 §5 七组用例(注入确定性序列;时间推进用既有 clock 机制,redis 变体真实等待 + 小 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 方法签名如需动,只加带默认值的关键字参数,旧实现兼容)。
- [ ] redis/breaker.py Lua: RECORD_FAILURE 增窗口 HINCRBY+率判据+streak;RECORD_SUCCESS 增 attempts 计数与 streak 衰减(closed_since 比较),**注意现有 HSET 整表重置不得清新域**;开路分支统一算 cooldown_eff。逐段对照保真。
- 验证: `pytest tests/unit -k breaker -q``pytest tests/integration -k "redis and (breaker or governance)" -q` 全绿;`pytest -m slow -k breaker` 真实等待变体绿(耗时可控)。
### T3 健康选源器
- [ ] `ports.py` OutcomeAwareSelector;`sources.py` HealthAwareSelector(rng 注入构造参数,P2C 用 rng 取两索引);`client.py` 装配。
- 测试(先红): 新文件 `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=False。
- 测试(先红): 既有 `tests/unit/test_retry*.py` 追加——① 单失败源仍首选、双失败移尾;② 并发两调用互不串 attempt_fails;③ record_outcome 喂数矩阵(五出口各断言);④ reasons/异常语义回归(旧用例全绿即可)。
- 验证: `pytest tests/unit -k retry -q` PASS;`make ci` 全绿。
### T5 文档与迁移标注
- [ ] `.env.example``migrations/chsanalyzer.md`(§6 三行审计表内容)、ARCHITECTURE.md 若 §7.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 元/轮(已有价格表),预算帽/守卫机制原样生效。