79 lines
8.3 KiB
Markdown
79 lines
8.3 KiB
Markdown
# 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 元/轮(已有价格表),预算帽/守卫机制原样生效。
|