docs: revise M2 design per independent review findings
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# M2 分布式里程碑设计:Redis 治理后端 + 背压 + Postgres 遥测 + pricing + Embedding + 压测 harness
|
# M2 分布式里程碑设计:Redis 治理后端 + 背压 + Postgres 遥测 + pricing + Embedding + 压测 harness
|
||||||
|
|
||||||
> **状态**: 草案(待 Claude 自审 → 独立 subagent 审 → 人类门)
|
> **状态**: 已过 Claude 自审与独立 subagent 审查(2026-07-20,2 Critical + 6 Important + 3 Minor 全部核验采纳修订:熔断契约时间用例 1:1 变体机制、stall entered_at 回归 CHS 口径、记账侧降级定案、Postgres 两级降级、embedding 遥测字段、probe_ttl 派生守卫对齐等)→ **待人类门**
|
||||||
> **依据**: ARCHITECTURE.md §7.3/§7.4/§7.8、ROADMAP §3(Q3 已拍板纳入 Embedding)、findings/2026-07-20-m2-soak-workload.md、M1 冻结契约(designs/2026-07-20-m1-core-design.md)、三份 reference 逐字调研(CHS 协调层 / Embedding 两版 / Postgres+pricing)
|
> **依据**: ARCHITECTURE.md §7.3/§7.4/§7.8、ROADMAP §3(Q3 已拍板纳入 Embedding)、findings/2026-07-20-m2-soak-workload.md、M1 冻结契约(designs/2026-07-20-m1-core-design.md)、三份 reference 逐字调研(CHS 协调层 / Embedding 两版 / Postgres+pricing)
|
||||||
> **硬约束**: M1 冻结的公共签名与双后端契约(`ports.py` 的 `RateLimiter`/`Permit`/`ProviderGate`/`GateDecision`/`GateUpdate`/`TelemetryRecorder` 18 字段)**不改动**;Redis 后端必须通过 `tests/contracts/` 同一套契约测试。
|
> **硬约束**: M1 冻结的公共签名与双后端契约(`ports.py` 的 `RateLimiter`/`Permit`/`ProviderGate`/`GateDecision`/`GateUpdate`/`TelemetryRecorder` 18 字段)**不改动**;Redis 后端必须通过 `tests/contracts/` 同一套契约测试。
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
- **窗口 id 用 Redis 服务器时钟**:`redis.time() → int(sec)//60`,多进程口径统一;Lua 内部 `TIME` 的 ms 只用于 lease 过期判定。**时钟三源分工不得合并**(窗口=服务器秒、lease=服务器 ms、stall 本地等待=monotonic)。
|
- **窗口 id 用 Redis 服务器时钟**:`redis.time() → int(sec)//60`,多进程口径统一;Lua 内部 `TIME` 的 ms 只用于 lease 过期判定。**时钟三源分工不得合并**(窗口=服务器秒、lease=服务器 ms、stall 本地等待=monotonic)。
|
||||||
- **检查顺序与判据**:全局并发→单源并发→全局 RPM→单源 RPM→全局 TPM→单源 TPM;并发/RPM 用 `>=`(占后即满),TPM 用 `used + est >`(预扣后是否超)。拒绝零副作用(仅顺带 `ZREMRANGEBYSCORE` 清过期 lease)。
|
- **检查顺序与判据**:全局并发→单源并发→全局 RPM→单源 RPM→全局 TPM→单源 TPM;并发/RPM 用 `>=`(占后即满),TPM 用 `used + est >`(预扣后是否超)。拒绝零副作用(仅顺带 `ZREMRANGEBYSCORE` 清过期 lease)。
|
||||||
- **租约双保险**:lease score 过期惰性清理 + 整 ZSET `PEXPIRE` 防僵尸 key;RPM/TPM key `EXPIRE 120s` 兜底。
|
- **租约双保险**:lease score 过期惰性清理 + 整 ZSET `PEXPIRE` 防僵尸 key;RPM/TPM key `EXPIRE 120s` 兜底。
|
||||||
- **settle 只结算 TPM**,`INCRBY delta`(可为负,与内存版 `max(0,·)` 的钳位差异由契约测试的 `<=` 容忍断言吸收),**落 acquire 时刻的窗口**而非当前窗口;RPM/并发不退款——熔断期白烧 RPM 由 M1 已交付的源冷却备忘缓解(retry.py:168-170)。
|
- **settle 只结算 TPM**,`INCRBY delta`(存储允许负值,保证跨窗口结算正确,CHS 原样);`source_stats` **读侧** clamp 到 ≥0,与内存版展示口径统一(库定义的展示规范,是对 CHS 的补充约定;契约现用例不触发负值场景,读侧 clamp 补 unit 测试)。退款**落 acquire 时刻的窗口**而非当前窗口;RPM/并发不退款——熔断期白烧 RPM 由 M1 已交付的源冷却备忘缓解(retry.py:168-170)。
|
||||||
- **release/settle 幂等**:Python 侧 flag + Lua ZREM 天然幂等;settle 仅成功后置位,失败可重试。
|
- **release/settle 幂等**:Python 侧 flag + Lua ZREM 天然幂等;settle 仅成功后置位,失败可重试。
|
||||||
- **Redis 异常 → `GovernanceBackendError`**(准入侧 fail-closed 报错不放行);settle/release 释放侧失败降级 warning 且不掩盖主异常(M1 勘误既定方向,ports 契约不变)。
|
- **Redis 异常 → `GovernanceBackendError`**(准入侧 fail-closed 报错不放行);settle/release 释放侧失败降级 warning 且不掩盖主异常(M1 勘误既定方向,ports 契约不变)。
|
||||||
- **进度键**:`pgw:limit:GLOBAL:{scope}:progress_ms`,SET 服务器 ms + `EXPIRE 3600`(TTL 远大于 stall_window,防进度键过期被误判"从未进展");`progress_age_s`:键缺失 → `inf`,时钟回拨 clamp 0。
|
- **进度键**:`pgw:limit:GLOBAL:{scope}:progress_ms`,SET 服务器 ms + `EXPIRE 3600`(TTL 远大于 stall_window,防进度键过期被误判"从未进展");`progress_age_s`:键缺失 → `inf`,时钟回拨 clamp 0。
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
| T1 fixture 增参 + 真实 Redis | `tests/contracts/conftest.py` 的 `limiter_factory`/`gate_factory` params 增 `"redis"`;无 `REDIS_URL` 时该 param skip;每 test 唯一 scope(uuid)隔离 | **推荐**:M1 预留的接入方式,测试体零改动 |
|
| T1 fixture 增参 + 真实 Redis | `tests/contracts/conftest.py` 的 `limiter_factory`/`gate_factory` params 增 `"redis"`;无 `REDIS_URL` 时该 param skip;每 test 唯一 scope(uuid)隔离 | **推荐**:M1 预留的接入方式,测试体零改动 |
|
||||||
| T2 fakeredis | 进程内模拟 | 否决:不执行真实 Lua,违反"Redis 测试用真实 Redis"规约 |
|
| T2 fakeredis | 进程内模拟 | 否决:不执行真实 Lua,违反"Redis 测试用真实 Redis"规约 |
|
||||||
|
|
||||||
时间语义用例(租约过期、窗口翻滚、进度 age 推进)FakeClock 对 Redis 不可用 → 该三类用例在 redis param 下改**小 TTL + 真实等待**变体(独立 integration 用例,移植 CHS `_await_window_headroom` 分钟翻滚防抖),契约文件本体不改。测试固定用 db3。
|
**时间语义用例的裁决(限流 2 例 + 熔断 8 例,即全部依赖 `clock.advance` 的用例)**:FakeClock 对 Redis 后端不可用(时间源是 Lua 内服务器 `TIME`,无法注入),且按比例缩放会破坏绝对值断言(如 progress age `41<age<43`)。方案:`clock` fixture 后端感知——memory param 返回 FakeClock;redis param 返回哨兵时钟,其 `advance()` 触发 `pytest.skip`(理由注明变体位置)。被 skip 的每个用例在 `tests/integration/test_redis_governance_time.py` 有 **1:1 对应的真实等待变体**(小时长配置:cooldown_s≈0.5、probe_ttl_s≈1.0、lease_ttl_s≈1.0,断言同名行为;窗口翻滚移植 CHS `_await_window_headroom` 防抖),plan 中附映射表逐条核对不漏。**两契约测试文件本体零改动**;不依赖时钟推进的用例(状态机、同 epoch fencing、幂等、六闸判定、settle 退款)在 redis param 下直接运行全绿。验收口径"双后端同一契约套件全绿"据此细化为:非时间用例双后端同套件,时间用例 memory 走契约文件、redis 走 1:1 变体——此口径请人类批准设计时一并认可(§13)。测试固定用 db3。
|
||||||
|
|
||||||
## 3. Redis 熔断(`backends/redis/breaker.py`)
|
## 3. Redis 熔断(`backends/redis/breaker.py`)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
- 探针租约 = `probe_until` 绝对 ms + 惰性重发(无看门狗);`release_probe` 置 `open_until=now` 使下家立即接管。
|
- 探针租约 = `probe_until` 绝对 ms + 惰性重发(无看门狗);`release_probe` 置 `open_until=now` 使下家立即接管。
|
||||||
- 探针失败/`force_open` → failures 顶格 threshold 后开断;普通失败累加。
|
- 探针失败/`force_open` → failures 顶格 threshold 后开断;普通失败累加。
|
||||||
- `retry_after_s(sources)` 取集合最小等待,clamp ≥0;空集合 ValueError(M1 契约)。
|
- `retry_after_s(sources)` 取集合最小等待,clamp ≥0;空集合 ValueError(M1 契约)。
|
||||||
- `BreakerConfig.probe_ttl_s` 保持显式配置(M1 冻结形态),CHS 的"container 自动算 `max(timeout)+5`"改为 §1 装配守卫——**方案对比**:自动计算(改冻结配置形态,否决)vs 显式 + 守卫(推荐,校验同效且不动公共类型)。
|
- `BreakerConfig.probe_ttl_s` 保持 M1 现状:显式配置或缺省派生(config 现派生式 `max(2×最大源超时, cooldown_s)`)。新增 §1 装配守卫 `probe_ttl_s ≥ max(timeout_s)+5`(CHS container 语义)后,派生式在 `max(timeout)<5s` 边角会自触守卫 → **派生式同步补第三项**:`max(2×最大源超时, cooldown_s, 最大源超时+5)`(config 内部实现,非冻结签名)。**方案对比**:全自动计算(CHS container 形态,剥夺显式配置能力,否决)vs 显式/派生 + 守卫(推荐,不动公共类型)。
|
||||||
|
|
||||||
## 4. 背压 stall(`middleware/retry.py` 增量)
|
## 4. 背压 stall(`middleware/retry.py` 增量)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
| S1 在 `_on_no_runnable` wait 分支内实现 | 首次无可运行源时记 `entered_at`(本地 monotonic);双条件 `local_waited > stall_window_s && progress_age_s() > stall_window_s` 同时成立 → 抛 `AllSourcesExhausted(reason="stalled")`;否则 sleep `poll_interval_s * (0.5+0.5*rng)`(jitter 防惊群,CHS governance.py:283-285) | **推荐**:挂接点已在 M1 预留,不新增洋葱层 |
|
| S1 在 `_on_no_runnable` wait 分支内实现 | 首次无可运行源时记 `entered_at`(本地 monotonic);双条件 `local_waited > stall_window_s && progress_age_s() > stall_window_s` 同时成立 → 抛 `AllSourcesExhausted(reason="stalled")`;否则 sleep `poll_interval_s * (0.5+0.5*rng)`(jitter 防惊群,CHS governance.py:283-285) | **推荐**:挂接点已在 M1 预留,不新增洋葱层 |
|
||||||
| S2 独立 BackpressureMW | 新中间件层 | 否决:改冻结层序;stall 判定与选源循环共享状态,拆层反而耦合 |
|
| S2 独立 BackpressureMW | 新中间件层 | 否决:改冻结层序;stall 判定与选源循环共享状态,拆层反而耦合 |
|
||||||
|
|
||||||
细则:`entered_at` 在每次 `chat()` 调用的重试循环内首次进入等待时记录,拿到 permit 即重置(等待是"连续无进展"语义,非累计);双条件缺一不判死(本地 monotonic 与 Redis 服务器时钟刻意不混用,CHS governance.py:270-281);`reason="stalled"` 已在 M1 错误模型 5 值枚举内,`retry_after_s` 取 `breaker.retry_after_s(全部源)`。`mark_progress` 调用点 M1 已就位(retry.py:213 成功即标)。行为对 memory/redis 两后端一致(`progress_age_s` 是端口方法)。
|
细则:`entered_at` 在每次 `chat()` 进入重试循环时记录一次、**循环内不重置**(CHS governance.py:207 口径逐字保留:`local_waited` 是"本次调用累计无果时长",含失败尝试与退避耗时——曾考虑"拿到 permit 即重置"的连续等待语义,为守保真纪律否决,不引入未声明行为改动);双条件缺一不判死(本地 monotonic 与 Redis 服务器时钟刻意不混用,CHS governance.py:270-281);`reason="stalled"` 已在 M1 错误模型 5 值枚举内,`retry_after_s` 取 `breaker.retry_after_s(全部源)`。`mark_progress` 调用点 M1 已就位(retry.py:213 成功即标)。行为对 memory/redis 两后端一致(`progress_age_s` 是端口方法)。
|
||||||
|
|
||||||
## 5. Postgres 遥测(`telemetry/postgres.py`,extra `postgres`)
|
## 5. Postgres 遥测(`telemetry/postgres.py`,extra `postgres`)
|
||||||
|
|
||||||
@@ -85,11 +85,11 @@
|
|||||||
|
|
||||||
| 方案 | 内容 | 裁决 |
|
| 方案 | 内容 | 裁决 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P1 DSN 自建池(lazy) | `PostgresRecorder(dsn)`,首次写入时 `asyncpg.create_pool`;建池/建表/写入任何失败 → `logger.warning` 一次性降级(置池 None 短路后续写),不冒泡 | **推荐**:与 SQLiteRecorder 对称,`from_settings` 只需 DSN 字符串 |
|
| P1 DSN 自建池(lazy) | `PostgresRecorder(dsn)`,首次写入时 `asyncpg.create_pool`;失败分两级降级(见细则),均不冒泡 | **推荐**:与 SQLiteRecorder 对称,`from_settings` 只需 DSN 字符串 |
|
||||||
| P2 注入 asyncpg.Pool | 池由业务创建传入 | 保留为构造函数可选参数(`pool=` 优先于 dsn 自建),满足"构造函数全量注入"路线;不作为 from_env 路径 |
|
| P2 注入 asyncpg.Pool | 池由业务创建传入 | 保留为构造函数可选参数(`pool=` 优先于 dsn 自建),满足"构造函数全量注入"路线;不作为 from_env 路径 |
|
||||||
| P3 复用 SQLAlchemy | — | 否决:重依赖,违依赖极简 |
|
| P3 复用 SQLAlchemy | — | 否决:重依赖,违依赖极简 |
|
||||||
|
|
||||||
细则:schema = 18 列同名 + `created_at timestamptz DEFAULT now()`,`call_id TEXT PRIMARY KEY`;幂等 `ON CONFLICT (call_id) DO NOTHING`;asyncpg 原生异步,无 to_thread/Lock;DSN 若带 SQLAlchemy 风格 `+asyncpg` 后缀先剥离(GovDoc verify 脚本教训);关闭走 `aclose()`(async),`GatewayClient.aclose` 增加对 recorder async close 的探测(内部行为,非冻结签名)。**降级语义**:构造不连库(lazy),Postgres 不可用时业务调用零感知(warning 一次,后续静默),与"缓存/遥测静默降级"铁律一致。写失败不重试(遥测丢一条 < 拖垮调用)。
|
细则:schema = 18 列同名 + `created_at timestamptz DEFAULT now()`,`call_id TEXT PRIMARY KEY`;幂等 `ON CONFLICT (call_id) DO NOTHING`;asyncpg 原生异步,无 to_thread/Lock;DSN 若带 SQLAlchemy 风格 `+asyncpg` 后缀先剥离(GovDoc verify 脚本教训);关闭走 `aclose()`(async),`GatewayClient.aclose` 增加对 recorder async close 的探测(内部行为,非冻结签名)。**降级语义(两级,均不冒泡)**:① 结构性失败(建池/建表失败)→ warning 一次后**永久降级**(池置 None 短路后续写);② 运行时写失败(池已建成后的单条 INSERT 异常)→ **逐条** warning 丢弃该行,**不降级不重试**(连接抖动由 asyncpg 池自恢复;避免浸泡开头一次抖动导致后续全程失遥测,守护 findings §4 不变量 2)。构造不连库(lazy),Postgres 不可用时业务调用零感知。
|
||||||
|
|
||||||
## 6. pricing(`pricing.py`)
|
## 6. pricing(`pricing.py`)
|
||||||
|
|
||||||
@@ -150,13 +150,13 @@ class EmbeddingClient:
|
|||||||
| 返回类型 | list[list[float]] | 归一化 np.ndarray | **list[list[float]]**(核心不依赖 numpy);ndarray/tensor 转换留 VT 业务侧 adapter(记入迁移文档) |
|
| 返回类型 | list[list[float]] | 归一化 np.ndarray | **list[list[float]]**(核心不依赖 numpy);ndarray/tensor 转换留 VT 业务侧 adapter(记入迁移文档) |
|
||||||
| L2 归一化 | 不做 | 强制做 | 构造参数 `normalize: bool`(默认 False;VT 装配传 True;纯 Python 实现,防除零 `max(norm,1e-12)` 保 VT 语义) |
|
| L2 归一化 | 不做 | 强制做 | 构造参数 `normalize: bool`(默认 False;VT 装配传 True;纯 Python 实现,防除零 `max(norm,1e-12)` 保 VT 语义) |
|
||||||
| 分批 | 内建 batch_size 切片 | 不分批整发 | **内建必填 batch_size**;批间串行(并发交 `gather_bounded`);每批 = 一次完整治理调用(独立 permit/熔断记账/遥测行) |
|
| 分批 | 内建 batch_size 切片 | 不分批整发 | **内建必填 batch_size**;批间串行(并发交 `gather_bounded`);每批 = 一次完整治理调用(独立 permit/熔断记账/遥测行) |
|
||||||
| 重试 | 自研退避(无上限无 jitter) | 零重试 | **有意放弃两者**,统一走库退避公式(base·2^n 封顶 + jitter,§7.2)与四分类驱动换源 |
|
| 重试 | 自研退避(次数有界但延迟无封顶、无 jitter) | 零重试 | **有意放弃两者**,统一走库退避公式(base·2^n 封顶 + jitter,ARCH §7.2)与四分类驱动换源 |
|
||||||
| 维度校验 | 校验不截断 | 不校验 | 可选 `expected_dim: int | None`,传入则逐批校验,不符抛 `ResultInvalidError`(坏结果不熔断);截断/降维永留业务侧 |
|
| 维度校验 | 校验不截断 | 不校验 | 可选 `expected_dim: int | None`,传入则逐批校验,不符抛 `ResultInvalidError`(坏结果不熔断);截断/降维永留业务侧 |
|
||||||
| 输入形态 | 仅 list | str 或 list | **仅 `list[str]`**(显式优于隐式);空 list 返回空响应不发请求 |
|
| 输入形态 | 仅 list | str 或 list | **仅 `list[str]`**(显式优于隐式);空 list 返回空响应不发请求 |
|
||||||
| usage | on_usage 回调 | 丢弃 | **有意放弃回调**,usage 直接入遥测(prompt_tokens=measured/estimated,completion_tokens=0),cost 经 pricing 换算 |
|
| usage | on_usage 回调 | 丢弃 | **有意放弃回调**,usage 直接入遥测(prompt_tokens=measured/estimated,completion_tokens=0),cost 经 pricing 换算 |
|
||||||
| index 排序 | 做 | 做 | 保留(响应按 index 重排保序) |
|
| index 排序 | 做 | 做 | 保留(响应按 index 重排保序) |
|
||||||
|
|
||||||
多批聚合:`EmbeddingResponse` 为全批合并(vectors 拼接、prompt_tokens 求和、latency 求和、call_id 取首批);遥测按批逐行(每批独立 call_id,parent_call_id 透传调用方值)。协议:`POST {base_url}/embeddings`(openai 兼容,与 chat 同 transport 类,错误翻译复用 §6.2 表——401→SourceDead、429/5xx→Transient、400→RequestRejected)。空向量/维度混乱 → `ResultInvalidError`。
|
多批聚合:`EmbeddingResponse` 为全批合并(vectors 拼接、prompt_tokens 求和、latency 求和、call_id 取首批;`usage_source` 任一批 estimated 则整体 estimated,保守口径);遥测按批逐行(每批独立 call_id,parent_call_id 透传调用方值),**遥测行字段定案**:`messages` = 本批 texts 的截断摘要 JSON(复用遥测既有摘要/截断口径,原文不整段入库——VT db 膨胀教训),`response` = `"<vectors n=N dim=D>"`(**向量绝不入库**),`thinking` = 空,`completion_tokens` = 0。治理参数沿用 scope 配置:`quota_full`(wait/fail_fast)与 stall 双条件判死对 embedding 循环同样适用(同一 `BackpressurePolicy`)。协议:`POST {base_url}/embeddings`(openai 兼容,与 chat 同 transport 类,错误翻译复用 §6.2 表——401→SourceDead、429/5xx→Transient、400→RequestRejected)。空向量/维度混乱 → `ResultInvalidError`。
|
||||||
|
|
||||||
## 8. 压测 harness(`tools/soak/`,不被 import、不入 pytest 门)
|
## 8. 压测 harness(`tools/soak/`,不被 import、不入 pytest 门)
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class EmbeddingClient:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `tools/soak/corpus.py` | 语料装载:harness.db traces→messages 还原器、telemetry.db 回放抽取、vt_frames/chs_images 组装 |
|
| `tools/soak/corpus.py` | 语料装载:harness.db traces→messages 还原器、telemetry.db 回放抽取、vt_frames/chs_images 组装 |
|
||||||
| `tools/soak/scenarios.py` | P1-P6 请求生成器(每场景一个 async 生成器,产出 chat/embed 调用参数) |
|
| `tools/soak/scenarios.py` | P1-P6 请求生成器(每场景一个 async 生成器,产出 chat/embed 调用参数) |
|
||||||
| `tools/soak/run_soak.py` | 入口:`--scenario P3 --budget-calls N --budget-tokens M`;开跑前 FLUSHDB(仅 db3,校验连接串含 `/3` 否则拒跑)+ `cache_namespace=run_id`;双上限(请求数/token)任一命中即停 |
|
| `tools/soak/run_soak.py` | 入口:`--scenario P3 --budget-calls N --budget-tokens M --workers K`;`--workers>1` 时按分片起 K 个**真实子进程**共享 Redis 治理状态(多 worker 验收的真多进程通道,P6 建议 K≥2);开跑前 FLUSHDB(仅 db3,校验连接串含 `/3` 否则拒跑)+ `cache_namespace=run_id`;双上限(请求数/token)任一命中即停。**soak 与 pytest 不并跑**(同用 db3,FLUSHDB 会清测试状态) |
|
||||||
| `tools/soak/scoreboard.py` | 跑后读本 run 遥测库断言 6 条硬不变量(findings §4);产报告至 `tests/outputs/soak/<run_id>.md` |
|
| `tools/soak/scoreboard.py` | 跑后读本 run 遥测库断言 6 条硬不变量(findings §4);产报告至 `tests/outputs/soak/<run_id>.md` |
|
||||||
|
|
||||||
故障源混编按 findings §3 配置(坏 key/黑洞/紧看门狗/紧闸源);回放请求掺 `cache_salt=run_id`。harness 用库自身的 `GatewayClient`/`EmbeddingClient` 与遥测(吃狗粮),不引入第三方压测框架。
|
故障源混编按 findings §3 配置(坏 key/黑洞/紧看门狗/紧闸源);回放请求掺 `cache_salt=run_id`。harness 用库自身的 `GatewayClient`/`EmbeddingClient` 与遥测(吃狗粮),不引入第三方压测框架。
|
||||||
@@ -183,7 +183,7 @@ class EmbeddingClient:
|
|||||||
|
|
||||||
**限流(CHS limiter.py + scripts.py)**:六闸顺序与判据/单 Lua 原子/ZSET 租约双保险/服务器时钟窗口/settle 落 acquire 窗口/负数 INCRBY/幂等 flag/拒绝零副作用/进度键 TTL 3600 —— 全部**保留**。`_BACKOFF_S=0.05` 固定轮询的阻塞 acquire —— **替换**为契约既有 `acquire`(M1 形态,poll_interval 可配)。key 前缀 `cclimit:` —— **替换**为 `pgw:limit:`。`LimiterError` —— **替换**为 `GovernanceBackendError`(M1 错误模型)。
|
**限流(CHS limiter.py + scripts.py)**:六闸顺序与判据/单 Lua 原子/ZSET 租约双保险/服务器时钟窗口/settle 落 acquire 窗口/负数 INCRBY/幂等 flag/拒绝零副作用/进度键 TTL 3600 —— 全部**保留**。`_BACKOFF_S=0.05` 固定轮询的阻塞 acquire —— **替换**为契约既有 `acquire`(M1 形态,poll_interval 可配)。key 前缀 `cclimit:` —— **替换**为 `pgw:limit:`。`LimiterError` —— **替换**为 `GovernanceBackendError`(M1 错误模型)。
|
||||||
|
|
||||||
**熔断(CHS provider_gate.py)**:五操作 Lua 语义/epoch 仅开断 +1/探针租约惰性重发/release_probe 立即可接管/force_open 顶格/时钟回拨 clamp —— 全部**保留**。probe_ttl 由 container 自动算 —— **替换**为显式配置 + 装配守卫(§3)。key 前缀 —— **替换**为 `pgw:gate:`。
|
**熔断(CHS provider_gate.py)**:五操作 Lua 语义/epoch 仅开断 +1/探针租约惰性重发/release_probe 立即可接管/force_open 顶格/时钟回拨 clamp —— 全部**保留**。probe_ttl 由 container 自动算 —— **替换**为显式/派生配置 + 装配守卫(§3)。key 前缀 —— **替换**为 `pgw:gate:`。
|
||||||
|
|
||||||
**背压(CHS governance.py)**:双条件公式/时钟分工/poll jitter —— **保留**;`ProviderUnavailableError("stalled")` —— **替换**为 `AllSourcesExhausted(reason="stalled")`(M1 错误模型翻译,语义等价)。
|
**背压(CHS governance.py)**:双条件公式/时钟分工/poll jitter —— **保留**;`ProviderUnavailableError("stalled")` —— **替换**为 `AllSourcesExhausted(reason="stalled")`(M1 错误模型翻译,语义等价)。
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class EmbeddingClient:
|
|||||||
## 10. 非功能维度
|
## 10. 非功能维度
|
||||||
|
|
||||||
- **并发与取消**:Redis 后端所有 await 点可被取消;单条 Lua 执行期不可中断但均为毫秒级短脚本;permit 取得后的取消由 M1 `try/finally` 结算释放路径覆盖(retry.py:160-170,行为不变,Redis 后端下集成测试重验);`acquire`/stall 等待的 sleep 可取消;Embedding 循环取消穿透同 RetryMW 契约(探针取消 → release_probe);Postgres 写入被取消 → 该行丢失,可接受(遥测非事务性承诺)。
|
- **并发与取消**:Redis 后端所有 await 点可被取消;单条 Lua 执行期不可中断但均为毫秒级短脚本;permit 取得后的取消由 M1 `try/finally` 结算释放路径覆盖(retry.py:160-170,行为不变,Redis 后端下集成测试重验);`acquire`/stall 等待的 sleep 可取消;Embedding 循环取消穿透同 RetryMW 契约(探针取消 → release_probe);Postgres 写入被取消 → 该行丢失,可接受(遥测非事务性承诺)。
|
||||||
- **降级方向**:限流/熔断 Redis 掉线 → 准入侧 `GovernanceBackendError` 报错不放行(六处 RedisError 捕获点全 fail-closed,CHS 同款);释放侧(settle/release/release_probe)失败 → warning 降级不掩盖主异常(M1 勘误);Postgres 遥测/缓存 → 静默降级;pricing 查不到 → cost=None + warning;pricing 文件坏 → 装配报错。
|
- **降级方向(逐方法定案,CHS 全 fail-closed 中"记账/释放侧"有意反转)**:**准入侧**——`try_acquire`/`acquire`/`try_enter` 及选源路径消费的 `source_stats`/`retry_after_s`,Redis 失败 → `GovernanceBackendError` 报错不放行;**记账/释放侧**——`settle`/`release`/`release_probe`(M1 勘误既定)**及本设计新定案的 `record_success`/`record_failure`/`mark_progress`**,失败 → warning 降级不冒泡不掩盖主异常/主响应(理由:调用已真实完成,抛错会丢弃真实成功响应或掩盖原始尝试异常;丢一次熔断记账最多延迟状态迁移且方向偏保守,epoch fencing 防污染;丢一次进度标记有 TTL 3600 的旧值兜底)。此定案需同步勘误 ARCHITECTURE §7.3/§7.4 释放侧措辞(批准后修订);Postgres 遥测/缓存 → 静默降级;pricing 查不到 → cost=None + warning;pricing 文件坏 → 装配报错。
|
||||||
- **幂等与重复**:settle/release/release_probe 幂等(契约已测,Redis 版靠 Lua 天然幂等 + flag);遥测 `ON CONFLICT DO NOTHING`;harness FLUSHDB 可重复;`EmbeddingClient.embed` 无副作用可重放(网关计费除外)。
|
- **幂等与重复**:settle/release/release_probe 幂等(契约已测,Redis 版靠 Lua 天然幂等 + flag);遥测 `ON CONFLICT DO NOTHING`;harness FLUSHDB 可重复;`EmbeddingClient.embed` 无副作用可重放(网关计费除外)。
|
||||||
- **持久化与原子性**:六闸判定与占用同一 Lua 原子;gate 每操作单 Lua 原子;跨 key(全局+单源)在同一脚本内一致;Postgres 单行 INSERT 原子;部分写入不产生半行。Redis 重启 = 治理状态清零(限额窗口/熔断状态重新累积),**有意接受**(CHS 同款,治理状态是软状态)。
|
- **持久化与原子性**:六闸判定与占用同一 Lua 原子;gate 每操作单 Lua 原子;跨 key(全局+单源)在同一脚本内一致;Postgres 单行 INSERT 原子;部分写入不产生半行。Redis 重启 = 治理状态清零(限额窗口/熔断状态重新累积),**有意接受**(CHS 同款,治理状态是软状态)。
|
||||||
|
|
||||||
@@ -203,9 +203,9 @@ class EmbeddingClient:
|
|||||||
**分类归属**:Redis 不可用(准入侧)→ `GovernanceBackendError`;stall → `AllSourcesExhausted("stalled")`;embedding 网关错误 → 四分类同款翻译;维度不符/空向量 → `ResultInvalidError`(不熔断);pricing 缺价 → 非错误(cost=None)。
|
**分类归属**:Redis 不可用(准入侧)→ `GovernanceBackendError`;stall → `AllSourcesExhausted("stalled")`;embedding 网关错误 → 四分类同款翻译;维度不符/空向量 → `ResultInvalidError`(不熔断);pricing 缺价 → 非错误(cost=None)。
|
||||||
|
|
||||||
**测试**(全部先红后绿):
|
**测试**(全部先红后绿):
|
||||||
1. **契约**:conftest 增 `redis` param(缺 `REDIS_URL` skip;唯一 scope 隔离;db3),两契约文件零改动全绿;时间语义补小 TTL 真实等待的 integration 变体。
|
1. **契约**:conftest 增 `redis` param(缺 `REDIS_URL` skip;唯一 scope 隔离;db3),契约文件本体零改动;非时间用例 redis param 直跑,时间语义 10 用例(限流 2 + 熔断 8)经哨兵时钟 skip 并在 `tests/integration/test_redis_governance_time.py` 以 1:1 真实等待变体覆盖(§2.3,plan 附映射表)。
|
||||||
2. **保真**:Lua 移植逐段比对 CHS 行号(plan 内设检查点);移植 CHS 三个跨连接集成用例(双连接池共享全局并发/全局 RPM/进度可见)。
|
2. **保真**:Lua 移植逐段比对 CHS 行号(plan 内设检查点);移植 CHS 三个跨连接集成用例(双连接池共享全局并发/全局 RPM/进度可见)。
|
||||||
3. **联合验证(③)**:多 client 双连接池下,多源换源 + 全局 RPM 不超配 + 熔断状态跨连接共享的集成测试;取消穿透在 Redis 后端下重验(in-flight 取消 → lease 释放)。
|
3. **联合验证(③)**:多 client 双连接池下,多源换源 + 全局 RPM 不超配 + 熔断状态跨连接共享的集成测试;取消穿透在 Redis 后端下重验(in-flight 取消 → lease 释放)。ROADMAP"多 worker 压测下全局限额真实生效"的验收通道 = pytest 双连接池等价验证(Redis 只见连接不见进程,Redis 后端无共享本地状态,连接≈进程)+ harness `--workers≥2` 真多进程冒烟,此口径请人类认可(§13)。
|
||||||
4. **stall**:memory 后端 FakeClock 推进双条件各自与同时成立的四象限;fail_fast 路径不受影响回归。
|
4. **stall**:memory 后端 FakeClock 推进双条件各自与同时成立的四象限;fail_fast 路径不受影响回归。
|
||||||
5. **Postgres**:真实实验室 Postgres?——**无现成实例,用 conda 环境本地起临时 postgres 或 docker;若都不可用则该集成测试标 skip 并在验收注明**(integration;幂等/降级/并发 50 写并落全部)。
|
5. **Postgres**:真实实验室 Postgres?——**无现成实例,用 conda 环境本地起临时 postgres 或 docker;若都不可用则该集成测试标 skip 并在验收注明**(integration;幂等/降级/并发 50 写并落全部)。
|
||||||
6. **Embedding**:unit(ScriptedTransport 分批/保序/归一化/维度校验/重试换源)+ e2e 真实网关冒烟(若网关有 embedding 端点;没有则 e2e 降为对 MiniMax chat 网关的 404 行为记录,unit 全覆盖)。
|
6. **Embedding**:unit(ScriptedTransport 分批/保序/归一化/维度校验/重试换源)+ e2e 真实网关冒烟(若网关有 embedding 端点;没有则 e2e 降为对 MiniMax chat 网关的 404 行为记录,unit 全覆盖)。
|
||||||
@@ -215,10 +215,13 @@ class EmbeddingClient:
|
|||||||
|
|
||||||
## 12. 内部顺序与交付物
|
## 12. 内部顺序与交付物
|
||||||
|
|
||||||
沿 ROADMAP §3:① Redis 限流 → ② Redis 熔断 → ③ 联合验证 → ④ stall → ⑤ Postgres 遥测 + ⑥ pricing(可并行)→ ⑦ Embedding → ⑧ harness(⑤-⑧ 相互独立,⑧ 依赖全部)。交付物:`backends/redis/{limiter,breaker}.py`、`middleware/retry.py` stall 增量、`telemetry/postgres.py`、`pricing.py`、`embedding.py` + transport 增量 + `ports.py`/`types.py` 新增(EmbeddingTransport/EmbeddingResponse,只增不改)、config 增量、`tools/soak/`、`.env.example` 回填、迁移文档 embedding 条目更新。
|
沿 ROADMAP §3 顺序(编号为本设计重排:ROADMAP ⑤=Postgres+pricing、⑥=Embedding,此处拆为 ⑤⑥⑦):① Redis 限流 → ② Redis 熔断 → ③ 联合验证 → ④ stall → ⑤ Postgres 遥测 + ⑥ pricing(可并行)→ ⑦ Embedding → ⑧ harness(⑤-⑧ 相互独立,⑧ 依赖全部)。交付物:`backends/redis/{limiter,breaker}.py`、`middleware/retry.py` stall 增量、`telemetry/postgres.py`、`pricing.py`、`embedding.py` + transport 增量 + `ports.py`/`types.py` 新增(EmbeddingTransport/EmbeddingResponse,只增不改)、config 增量、`tools/soak/`、`.env.example` 回填、迁移文档 embedding 条目更新。
|
||||||
|
|
||||||
## 13. 开放问题(设计内已给提议,批准时可一并裁决)
|
## 13. 开放问题(设计内已给提议,批准时可一并裁决)
|
||||||
|
|
||||||
1. §8.1 三项签字(预算/网关保护/P6 比例)。
|
1. §8.1 三项签字(预算/网关保护/P6 比例)。
|
||||||
2. Postgres 集成测试环境:实验室有无可用 Postgres 实例?(无则按 §11.5 降级方案)
|
2. Postgres 集成测试环境:实验室有无可用 Postgres 实例?(无则按 §11.5 降级方案)
|
||||||
3. 真实网关是否有 embedding 端点可供 e2e?(无则按 §11.6 降级方案)
|
3. 真实网关是否有 embedding 端点可供 e2e?(无则按 §11.6 降级方案)
|
||||||
|
4. 契约验收口径认可(§2.3):时间语义 10 用例 redis param 下 skip、由 1:1 真实等待 integration 变体覆盖——"双后端同一契约套件全绿"含此细化。
|
||||||
|
5. 多 worker 验收口径认可(§11.3):pytest 双连接池等价 + harness `--workers≥2` 真多进程冒烟。
|
||||||
|
6. 记账/释放侧降级定案认可(§10):`record_success`/`record_failure`/`mark_progress` Redis 失败降级 warning(CHS 原版报错,有意反转),批准后勘误 ARCHITECTURE。
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ date: 2026-07-21
|
|||||||
| 轴 | 选定 | 理由 |
|
| 轴 | 选定 | 理由 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Redis 限流 | 单条六道闸 Lua 逐字移植(CHS scripts.py),key 前缀 `pgw:limit:` | 生产验证语义 + 保真成本最低 |
|
| Redis 限流 | 单条六道闸 Lua 逐字移植(CHS scripts.py),key 前缀 `pgw:limit:` | 生产验证语义 + 保真成本最低 |
|
||||||
| Redis 熔断 | 五操作 Lua 逐字移植(provider_gate),epoch 仅开断 +1;probe_ttl 显式配置 + 装配守卫 | 不动 M1 冻结的 BreakerConfig |
|
| Redis 熔断 | 五操作 Lua 逐字移植(provider_gate),epoch 仅开断 +1;probe_ttl 显式/派生配置 + 装配守卫(派生式补 `timeout+5` 项) | 不动 M1 冻结的 BreakerConfig |
|
||||||
| 契约测试 | conftest fixture 增 `redis` param + 真实 Redis(db3);时间语义用小 TTL 真实等待变体 | M1 预留接入方式,契约文件零改动 |
|
| 契约测试 | conftest fixture 增 `redis` param + 真实 Redis(db3);时间语义 10 用例(限流 2+熔断 8)redis param 下经哨兵时钟 skip,由 1:1 真实等待 integration 变体覆盖 | 契约文件本体零改动;FakeClock 对服务器时钟不可注入,缩放会破坏绝对值断言 |
|
||||||
| 背压 stall | 在 RetryMW `_on_no_runnable` wait 分支内做双条件判死 + poll jitter | 挂接点 M1 已预留,不新增洋葱层 |
|
| 背压 stall | 在 RetryMW `_on_no_runnable` wait 分支内做双条件判死 + poll jitter | 挂接点 M1 已预留,不新增洋葱层 |
|
||||||
| Postgres 遥测 | DSN 自建 lazy 池(可注入 pool),失败静默降级,`ON CONFLICT DO NOTHING` | 与 SQLiteRecorder 对称;GovDoc taskrun 蓝本反转降级方向 |
|
| Postgres 遥测 | DSN 自建 lazy 池(可注入 pool),失败静默降级,`ON CONFLICT DO NOTHING` | 与 SQLiteRecorder 对称;GovDoc taskrun 蓝本反转降级方向 |
|
||||||
| pricing | JSON 价格表文件(`PGW_PRICING_PATH`)/dict 注入,零内置单价;计算点在 TelemetryEmitter | 中转网关计费非官方价,内置表必然过时(P5) |
|
| pricing | JSON 价格表文件(`PGW_PRICING_PATH`)/dict 注入,零内置单价;计算点在 TelemetryEmitter | 中转网关计费非官方价,内置表必然过时(P5) |
|
||||||
|
|||||||
Reference in New Issue
Block a user