Files
PolyGateway/research-wiki/designs/2026-07-20-m2-distributed-design.md
T

228 lines
26 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 分布式里程碑设计:Redis 治理后端 + 背压 + Postgres 遥测 + pricing + Embedding + 压测 harness
> **状态**: ✅ **已实现并合并 main(2026-07-21)**。流程:Claude 自审 → 独立 subagent 审(2C+6I+3M 采纳)→ 人类批准(修正:时间变体真实等待不缩放、token 硬顶 2 亿、PG 专用库 polygateway)→ plan(独立审 9I+10M 采纳)→ T0-T12 实现 → 独立 verifier(0 Critical,3I+4M 当日修复,见 findings/m2-verifier-fixes.md)→ 快进合并。实现期勘误 2 条已回写本文与 ARCHITECTURE:限流"0=闸不启用"为对 CHS 的有意偏离(§9);settle 置位时机偏离已在代码 docstring 声明。
> **依据**: 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/` 同一套契约测试。
## 0. 范围与非目标
**范围(七项 + harness)**: ① Redis 限流六道闸 ② Redis 熔断(epoch fencing)③ 多源 × Redis 联合验证 ④ 背压 stall 判定 ⑤ `telemetry/postgres.py``pricing.py` 成本入遥测 ⑦ Embedding 客户端 ⑧ 真实数据压测 harness(`tools/`,不入 pytest 门)。
**非目标**: OCR(M3)、音频、SDK transport、泛化中间件洋葱使其类型无关(见 §7 方案 G1 否决)、embedding 结果缓存(下游索引本就持久化向量,YAGNI)。
**新增依赖(均须人类确认,随本设计一并批)**: `redis`(已是 optional extra,M1 缓存已用)、`asyncpg` → 新 extra `postgres`。Embedding 走 httpx,零新依赖。
## 1. 装配变化(config + 工厂)
| 键 | 变化 |
|---|---|
| `PGW_LIMITER_BACKEND` / `PGW_BREAKER_BACKEND` | 解禁 `redis`(删除 config.py:254-255 硬拒);取 `redis` 时强校验 `REDIS_URL` 存在 |
| `PGW_TELEMETRY_BACKEND` | 白名单扩为 `sqlite/postgres/none`;`postgres` 时强校验新键 `PGW_TELEMETRY_PG_DSN` |
| `PGW_PRICING_PATH` | 新增,可选;指向价格表 JSON 文件;缺省 = 无 pricing,cost 恒 None(现状) |
| `EMBED__{PROVIDER}__{N}__{FIELD}` | Embedding 源沿用既有多源命名约定,`EmbeddingClient.from_env(scope="EMBED")` 装配 |
| `{SCOPE}__BATCH_SIZE` | Embedding scope 专用,必填(embedding 分批是行为关键,不设默认) |
装配期守卫(ARCHITECTURE §7.3/§7.4 契约补强,后端无关,对 memory/redis 一体执法):`max(timeout_s) ≤ lease_ttl_s``probe_ttl_s ≥ max(timeout_s) + 5``stall_window_s ≥ max(ttft_timeout_s)`(未配 ttft 的源跳过)。违反 → 装配报错拒绝启动。M1 已实现的部分保持,缺的补齐(plan 核对)。
共享语义不变:多逻辑角色共享全局闸 = 显式把同一 `RedisLimiter`/`RedisGate` 实例传入多次 `from_env(limiter=..., breaker=...)`;Redis 后端天然跨进程共享(key 按 scope+source),**同 scope 的多进程 worker 无需显式传实例即共享状态**——这是与内存版唯一的行为差异,属 Redis 后端的目的本身。
## 2. Redis 限流六道闸(`backends/redis/limiter.py`)
### 2.1 方案对比
| 方案 | 内容 | 裁决 |
|---|---|---|
| L1 单条 Lua 逐字移植 | CHS `scripts.py` ACQUIRE/RELEASE/SETTLE/STATS/PROGRESS_* 六脚本原样移植,读-判-占一次 EVAL 原子完成 | **推荐**:语义已被 CHS 生产验证 + 契约测试钉死;保真校验成本最低 |
| L2 WATCH/MULTI 事务 | redis-py 乐观锁重写 | 否决:多往返、竞态重试风暴、六闸联合判定难以原子化 |
| L3 每闸独立脚本 + 补偿 | 六个小脚本,失败回滚已占闸 | 否决:原子性碎裂,部分占用的补偿路径本身可能失败 |
### 2.2 关键语义(逐字保真点,plan 中逐条比对 CHS 行号)
- **Key 布局**(前缀 CHS `cclimit:``pgw:limit:`):并发 = ZSET `pgw:limit:{GLOBAL|scope}:{...}:lease`(member=lease_id, score=过期时刻 ms);RPM/TPM = string 计数 `...:rpm:{win}` / `...:tpm:{win}`,窗口后缀由 Python 侧预生成。
- **窗口 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)。
- **租约双保险**:lease score 过期惰性清理 + 整 ZSET `PEXPIRE` 防僵尸 key;RPM/TPM key `EXPIRE 120s` 兜底。
- **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 仅成功后置位,失败可重试。
- **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。
- 秒(契约)↔ 毫秒(Redis 内部)换算是后端私事,不进端口。
### 2.3 契约测试接入
| 方案 | 内容 | 裁决 |
|---|---|---|
| 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"规约 |
**时间语义用例的裁决(限流 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 对应的真实等待变体**——**人类拍板(2026-07-20):不缩放时长,用真实量级配置真实等待**(cooldown_s/probe_ttl_s 取契约同值或贴近生产的值,如 cooldown 60s、probe_ttl 120s;整文件预计 10-20 分钟,标记 `@pytest.mark.slow`,缺 `REDIS_URL` 自动 skip;窗口翻滚移植 CHS `_await_window_headroom` 防抖),plan 中附映射表逐条核对不漏。**两契约测试文件本体零改动**;不依赖时钟推进的用例(状态机、同 epoch fencing、幂等、六闸判定、settle 退款)在 redis param 下直接运行全绿。验收口径"双后端同一契约套件全绿"据此细化为:非时间用例双后端同套件,时间用例 memory 走契约文件、redis 走 1:1 变体——此口径请人类批准设计时一并认可(§13)。测试固定用 db3。
## 3. Redis 熔断(`backends/redis/breaker.py`)
方案同构于 §2(L1 逐字移植 CHS `provider_gate.py` 五条 Lua,否决理由同,不重复列)。Key:每源一个 HASH `pgw:gate:{scope}:{source}`,字段 state/epoch/failures/open_until/probe_until/probe_owner。
**保真点**(与 M1 内存版契约测试逐条对应,Redis 版必须同绿):
- epoch **仅在进入 OPEN 时 +1**(record_failure 开断路径);try_enter 发探针、record_success、release_probe 均不递增。
- fencing 判据:普通写回 `state==closed && epoch==entry.epoch`;探针写回 `state==half_open && epoch匹配 && owner==entry.probe_owner`;不匹配 → `applied=False` 快照返回。`allowed=False` 的决定伪造写回在 Python 侧拒绝(ValueError,与内存版一致)。
- 探针租约 = `probe_until` 绝对 ms + 惰性重发(无看门狗);`release_probe``open_until=now` 使下家立即接管。
- 探针失败/`force_open` → failures 顶格 threshold 后开断;普通失败累加。
- `retry_after_s(sources)` 取集合最小等待,clamp ≥0;空集合 ValueError(M1 契约)。
- `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` 增量)
| 方案 | 内容 | 裁决 |
|---|---|---|
| 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 判定与选源循环共享状态,拆层反而耦合 |
细则:`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`)
**参考现实**:三项目均无 Postgres 遥测先例;工程蓝本取 GovDoc `PostgresTaskStore`(asyncpg、`$n` 占位、`CREATE TABLE IF NOT EXISTS``ON CONFLICT DO NOTHING`),但其"失败冒泡"方向与遥测铁律相反,**降级方向反向处理**。
| 方案 | 内容 | 裁决 |
|---|---|---|
| P1 DSN 自建池(lazy) | `PostgresRecorder(dsn)`,首次写入时 `asyncpg.create_pool`;失败分两级降级(见细则),均不冒泡 | **推荐**:与 SQLiteRecorder 对称,`from_settings` 只需 DSN 字符串 |
| P2 注入 asyncpg.Pool | 池由业务创建传入 | 保留为构造函数可选参数(`pool=` 优先于 dsn 自建),满足"构造函数全量注入"路线;不作为 from_env 路径 |
| 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 的探测(内部行为,非冻结签名)。**降级语义(两级,均不冒泡)**:① 结构性失败(建池/建表失败)→ warning 一次后**永久降级**(池置 None 短路后续写);② 运行时写失败(池已建成后的单条 INSERT 异常)→ **逐条** warning 丢弃该行,**不降级不重试**(连接抖动由 asyncpg 池自恢复;避免浸泡开头一次抖动导致后续全程失遥测,守护 findings §4 不变量 2)。构造不连库(lazy),Postgres 不可用时业务调用零感知。
## 6. pricing(`pricing.py`)
**参考现实**:三项目零先例,从头设计。
| 方案 | 内容 | 裁决 |
|---|---|---|
| B1 JSON 价格表文件 + 注入 | `PricingTable.from_file(path)`(`PGW_PRICING_PATH`)或 `PricingTable(dict)` 直接注入;条目 `model → {input_per_1m, output_per_1m}` | **推荐**:价格随网关计费变化,由使用方维护 |
| B2 库内置默认价格表 | 硬编码常见模型单价 | 否决:必然过时的默认值掩盖真实成本(P5);实验室走中转网关,计费非官方价 |
| B3 env 平铺单价键 | `PRICING__<model>__INPUT` | 否决:model 名含 `.`/`-`,env 键名不友好 |
细则:`cost = prompt/1e6*input + completion/1e6*output`(float,币种由使用方全表统一口径,库不设币种字段——18 字段冻结);查不到 model → cost=None + 每 model 仅首次 warning(防日志风暴),不阻塞调用;计算点 = `TelemetryEmitter`(cost=None 的唯一现存占位点 middleware/telemetry.py:146),Emitter 构造增可选 `pricing: PricingTable | None`——遥测单一 helper 铁律不破。缓存命中行 cost=0(未产生新调用)。文件解析失败 → 装配报错(配置类失败 fail-loud,非运行时降级)。
## 7. Embedding 客户端(`embedding.py` + `transports/openai_compat.py` 增量)
### 7.1 方案对比(治理栈复用方式)
| 方案 | 内容 | 裁决 |
|---|---|---|
| G1 泛化中间件洋葱 | RetryMW/遥测/洋葱全部泛型化为请求类型无关 | 否决:动 M1 冻结核心,收益仅一个新调用形态,典型 gold-plating |
| G2 独立 `EmbeddingClient` + 复用后端与算法件 | 新类持自己的精简治理循环(选源→冷却备忘→熔断门→限流 permit→transport→错误分类→退避),**直接复用**:`RateLimiter`/`ProviderGate` 端口及两种后端、`errors.py` 四分类、退避公式、`SourceCooldownMemo``TelemetryEmitter``SourceConfig`/选源器 | **推荐**:零改冻结面;循环逻辑与 RetryMW 存在有限重复,以"共享算法件、循环骨架各自持有"为界(chat 循环含流式/结构化/缓存分支,embedding 循环无,强行合一才是复制) |
| G3 塞进 chat 洋葱 | embedding 请求伪装 ChatRequest | 否决:messages/stream/structured 全不适用,类型欺骗 |
### 7.2 新公共签名(冻结候选,过人类门后与 M1 同等约束)
```python
@dataclass(frozen=True)
class EmbeddingResponse:
vectors: list[list[float]] # 与输入等长、保序
dim: int
model: str
provider: str
prompt_tokens: int
usage_source: str # measured | estimated
latency_ms: int
call_id: str
source_name: str
cost: float | None = None
```
```python
class EmbeddingTransport(Protocol): # ports.py 新增
async def embed(self, *, texts: list[str], source: SourceConfig,
call_id: str) -> EmbeddingTransportResult: ...
# EmbeddingTransportResult(types.py 新增, frozen): vectors/dim/prompt_tokens/usage_source/raw
class EmbeddingClient:
async def embed(self, texts: list[str], *, session_id: str | None = None,
parent_call_id: str | None = None) -> EmbeddingResponse: ...
# from_env(scope="EMBED", limiter=..., breaker=..., telemetry=...) 与 GatewayClient 工厂对称
```
### 7.3 行为裁决(两版审计的分歧点)
| 分歧 | GovDoc | VT | 库裁决 |
|---|---|---|---|
| 同步/异步 | httpx async | 同步 SDK | **async + httpx**(纯 asyncio 中立铁律);VT 迁移侧自包同步壳 |
| 返回类型 | 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 语义) |
| 分批 | 内建 batch_size 切片 | 不分批整发 | **内建必填 batch_size**;批间串行(并发交 `gather_bounded`);每批 = 一次完整治理调用(独立 permit/熔断记账/遥测行) |
| 重试 | 自研退避(次数有界但延迟无封顶、无 jitter) | 零重试 | **有意放弃两者**,统一走库退避公式(base·2^n 封顶 + jitter,ARCH §7.2)与四分类驱动换源 |
| 维度校验 | 校验不截断 | 不校验 | 可选 `expected_dim: int | None`,传入则逐批校验,不符抛 `ResultInvalidError`(坏结果不熔断);截断/降维永留业务侧 |
| 输入形态 | 仅 list | str 或 list | **仅 `list[str]`**(显式优于隐式);空 list 返回空响应不发请求 |
| usage | on_usage 回调 | 丢弃 | **有意放弃回调**,usage 直接入遥测(prompt_tokens=measured/estimated,completion_tokens=0),cost 经 pricing 换算 |
| index 排序 | 做 | 做 | 保留(响应按 index 重排保序) |
多批聚合:`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 门)
结构(场景矩阵与不变量以 findings 文档为准,此处只定工程形态):
| 组件 | 职责 |
|---|---|
| `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/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` |
故障源混编按 findings §3 配置(坏 key/黑洞/紧看门狗/紧闸源);回放请求掺 `cache_salt=run_id`。harness 用库自身的 `GatewayClient`/`EmbeddingClient` 与遥测(吃狗粮),不引入第三方压测框架。
### 8.1 待人类签字项(harness 实现前拍板,可在批准本设计时一并给)
| 项 | 提议值(可改) |
|---|---|
| 预算上限 | P1≤500 次 / P2≤450 次 / P3+P4≤2500 次 / P5≤1000 次 / P6≤8000 次或 3h;全程 token 硬顶 **2 亿**(输入+输出合计,按遥测实测累计;2026-07-20 人类签字,自提议值 5000 万上调) |
| 网关保护 | harness 全局闸:max_concurrency=100、全局 RPM=600;跑 P6 建议夜间时段(具体由人类定) |
| P6 混合比例 | P1 10% / P2 20% / P3 50%(其中 3/10 为重复 messages 走缓存双向,即 P4)/ P5 20% |
## 9. 旧版行为审计(迁移类,逐条标注)
**限流(CHS limiter.py + scripts.py)**:六闸顺序与判据/单 Lua 原子/ZSET 租约双保险/服务器时钟窗口/settle 落 acquire 窗口/负数 INCRBY/幂等 flag/拒绝零副作用/进度键 TTL 3600 —— 全部**保留**。限额 0 的语义 —— **有意偏离(勘误 2026-07-20 计划审查)**:CHS Lua 无 `>0` 守卫,0 在其语义下是全拒(靠传大数表示不限);库契约(M1 冻结)定 0=该闸不启用,库版 Lua 显式新增 `limit>0` 守卫。`_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 governance.py)**:双条件公式/时钟分工/poll jitter —— **保留**;`ProviderUnavailableError("stalled")` —— **替换**为 `AllSourcesExhausted(reason="stalled")`(M1 错误模型翻译,语义等价)。
**Embedding(GovDoc/VT)**:逐条裁决见 §7.3 表(该表即审计,含三处"有意放弃":GovDoc 自研退避、on_usage 回调、VT 同步接口)。
**遥测(GovDoc taskrun 蓝本)**:asyncpg 用法/占位符/幂等写法 —— **保留**;失败冒泡方向 —— **有意反转**为静默降级(遥测铁律)。
## 10. 非功能维度
- **并发与取消**:Redis 后端所有 await 点可被取消;单条 Lua 执行期不可中断但均为毫秒级短脚本;permit 取得后的取消由 M1 `try/finally` 结算释放路径覆盖(retry.py:160-170,行为不变,Redis 后端下集成测试重验);`acquire`/stall 等待的 sleep 可取消;Embedding 循环取消穿透同 RetryMW 契约(探针取消 → release_probe);Postgres 写入被取消 → 该行丢失,可接受(遥测非事务性承诺)。
- **降级方向(逐方法定案,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` 无副作用可重放(网关计费除外)。
- **持久化与原子性**:六闸判定与占用同一 Lua 原子;gate 每操作单 Lua 原子;跨 key(全局+单源)在同一脚本内一致;Postgres 单行 INSERT 原子;部分写入不产生半行。Redis 重启 = 治理状态清零(限额窗口/熔断状态重新累积),**有意接受**(CHS 同款,治理状态是软状态)。
## 11. 错误分类与测试策略
**分类归属**:Redis 不可用(准入侧)→ `GovernanceBackendError`;stall → `AllSourcesExhausted("stalled")`;embedding 网关错误 → 四分类同款翻译;维度不符/空向量 → `ResultInvalidError`(不熔断);pricing 缺价 → 非错误(cost=None)。
**测试**(全部先红后绿):
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/进度可见)。
3. **联合验证(③)**:多 client 双连接池下,多源换源 + 全局 RPM 不超配 + 熔断状态跨连接共享的集成测试;取消穿透在 Redis 后端下重验(in-flight 取消 → lease 释放)。ROADMAP"多 worker 压测下全局限额真实生效"的验收通道 = pytest 双连接池等价验证(Redis 只见连接不见进程,Redis 后端无共享本地状态,连接≈进程)+ harness `--workers≥2` 真多进程冒烟,此口径请人类认可(§13)。
4. **stall**:memory 后端 FakeClock 推进双条件各自与同时成立的四象限;fail_fast 路径不受影响回归。
5. **Postgres**:用实验室实例的**专用库 `polygateway`**(2026-07-20 已创建;该实例上有 app/chs_prod/mimiciv 等在用库,遥测测试严禁指向,与 Redis db3 同款隔离纪律);DSN 走 `.env``PGW_TELEMETRY_PG_DSN`(已配,不入任何提交文件),缺键自动 skip(integration;幂等/降级/并发 50 写并落全部)。
6. **Embedding**:unit(ScriptedTransport 分批/保序/归一化/维度校验/重试换源)+ e2e 真实网关冒烟(若网关有 embedding 端点;没有则 e2e 降为对 MiniMax chat 网关的 404 行为记录,unit 全覆盖)。
7. **harness**:本体不入 pytest;`corpus.py` 的 traces 还原器与不变量断言函数给 unit 测试(纯函数)。
覆盖率目标沿用 ≥80%;遥测/缓存降级、限流结算退款、熔断开路半开、Redis 掉线方向仍是一等测试对象。
## 12. 内部顺序与交付物
沿 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. 开放问题(2026-07-20 人类门全部裁决)
1. §8.1 三项签字:✅ 按提议值,唯 token 硬顶上调至 2 亿。
2. Postgres 测试环境:✅ 实验室实例专用库 `polygateway`(见 §11.5)。
3. 网关 embedding 端点:人类未明示,按推荐默认**实现时发一次真实探测请求**,有则 e2e、无则记录降级(§11.6);可随时推翻。
4. 契约验收口径:✅ 认可,且修正为**真实量级时长真实等待、不缩放**(§2.3)。
5. 多 worker 验收口径:✅ 认可(pytest 双连接池等价 + harness 真多进程)。
6. 记账/释放侧降级反转:✅ 认可,ARCHITECTURE 勘误随本次提交。