docs: correct the stale throughput numbers and wiki state
独立验证发现的 3 处文档欠账:
③ 两处代码内注释还挂着已作废的吞吐估算,`.env.example`/README/
CHANGELOG/ARCHITECTURE 四处早已改成实测口径:
- `config.py` 的 `# 4 条 ≈ 32 行/秒(实测…)` —— "32 行/秒"正是设计
§10 修订 #1 判定"偏乐观一倍"并作废的估算值,却挂着"实测"二字;
- `postgres.py` 的 `pool_max` docstring 写着 `稳态吞吐 ≈ pool_max /
RTT`,正是设计要求下游**不要**用的那个公式。
两处统一为实测值: RTT ≈ 123ms 上 `pool_max=4` 约 15.6 行/秒
(50 行并发批 3.2s)。设计 §8 与计划 T7 里残留的同一公式一并标注作废。
④ 文档写 `acquire(timeout=剩余预算)`,实现传的是完整预算(行为无害,
外层 `asyncio.timeout` 才是真正上界)。**改文档不改代码**: 设计
§3.1、计划 T3、ARCH §7.8 三处对齐,并写明为什么内层不再算剩余量。
⑤ wiki 登记页与正文状态漂移: design 登记页仍写"待人类审"(正文已是
"已实施")、plan 登记页写"正文 326 行"(实际 380)、log.md 末条停在
T0 之前。三处校正,T1-T8 补登记,rebuild_index。
另补一条独立验证在真实 PG 上发现的语义细节: 本地池饱和造成的丢行走
**行级丢弃**,`degraded` 保持 False,只有 `dropped_rows` 增长——只按
`degraded` 配告警的下游会完全看不见这类丢行,而它恰是 `pool_max` 配小
了的唯一信号。README / .env.example / ARCHITECTURE / CHANGELOG 各补一句。
This commit is contained in:
@@ -87,7 +87,7 @@ issue 建议"让指向同一 DSN 的多个 recorder 共享一个池"。这条路
|
||||
- **只暴露 `max_size`**(理由见 §2)。稳态占用从"40 条常驻"变成"实际并发,闲时 0"。
|
||||
- 整次写入(`_ensure_ready` + `acquire` + `execute`)由 `asyncio.timeout` 包一层**硬预算**,超时按行级丢弃。这把"遥测绝不拖垮业务"从"靠各处 timeout 参数凑"升级为一条可陈述、可测试的保证。
|
||||
- `acquire` 必须显式传 timeout。今天 `postgres.py:238` 的 `pool.acquire()` **无超时**(asyncpg 缺省 `timeout=None` = 无限等待),池满时会无限期挂在业务路径上——现状因 `max_size=10` 而未暴露,`max_size=4` 后必须补齐。
|
||||
- **不得用 `async with pool.acquire(...)`(Codex 审查,2026-08-24,已核实)**。`Pool.release()` 是 `await asyncio.shield(ch.release(timeout))`,且该 timeout **默认取 acquire 时记录的 `ch._timeout`**(asyncpg `pool.py:886-889, 930-937`)。外层预算到期时 cancel 在 `execute` 处抛出,异常传播中执行 `async with` 的 `__aexit__`,此时**没有新的 cancel 投递**,那个 shielded release 会正常等到完成——于是业务路径的真实上界是 **≈ 2 × 预算**,而不是文档原先承诺的一个预算。故改为显式 `con = await pool.acquire(timeout=剩余预算)` + `finally: await pool.release(con, timeout=<小的独立上限>)`,释放超时则 `con.terminate()`。承诺相应精确化为: **主写入尝试 ≤ 预算,释放路径独立有界**。
|
||||
- **不得用 `async with pool.acquire(...)`(Codex 审查,2026-08-24,已核实)**。`Pool.release()` 是 `await asyncio.shield(ch.release(timeout))`,且该 timeout **默认取 acquire 时记录的 `ch._timeout`**(asyncpg `pool.py:886-889, 930-937`)。外层预算到期时 cancel 在 `execute` 处抛出,异常传播中执行 `async with` 的 `__aexit__`,此时**没有新的 cancel 投递**,那个 shielded release 会正常等到完成——于是业务路径的真实上界是 **≈ 2 × 预算**,而不是文档原先承诺的一个预算。故改为显式 `con = await pool.acquire(timeout=self._write_timeout_s)` + `finally: await pool.release(con, timeout=<小的独立上限>)`,释放超时则 `con.terminate()`。**acquire 传的是完整预算而非剩余预算**(实施期核定,T3): 真正的上界是外层那一层 `asyncio.timeout`,内层再算一次剩余量只是把同一个上界写两遍,徒增出错面;实测总耗时正好等于预算。承诺相应精确化为: **主写入尝试 ≤ 预算,释放路径独立有界**。
|
||||
- `CancelledError` 穿透由测试钉死: `asyncio.timeout` 只把自己触发的 cancel 转成 `TimeoutError`,外部取消照常以 `CancelledError` 冒出(实测确认,Codex 独立复现)。**实现纪律**: 降级路径(节流日志、tracker 更新、release 收尾)一律不得 `except CancelledError` 而不 re-raise;`except TimeoutError` 必须排在 `except Exception` 之前;严禁裸 `except BaseException`(铁律"取消可穿透")。
|
||||
|
||||
### 3.2 B 组 · 失败三分与冷却降级
|
||||
@@ -123,7 +123,7 @@ issue 建议"让指向同一 DSN 的多个 recorder 共享一个池"。这条路
|
||||
|
||||
| 能力 | 行为 |
|
||||
|---|---|
|
||||
| 进入降级 | warning 一条,含原因分档与恢复条件(冷却剩余 / "需重启") |
|
||||
| 进入降级 | 一条日志,含原因分档与恢复条件(冷却剩余 / "需重启");**级别由 `fatal` 决定且只在这一处决定**——致命档 error(人配错了,不会自愈)、其余 warning。recorder 侧不得再复制一条(实施期更正 #4) |
|
||||
| 降级期间 | 按丢弃行数与时间**节流复述**(不刷屏,也不静默)——这一条是 §1.4 的直接钉子 |
|
||||
| 恢复 | info 一条,报告"期间丢弃 N 行" |
|
||||
| 快照 | `TelemetryStatus` frozen dataclass(放 `types.py`,与 `SourceStats` 同一先例): `degraded` / `fatal` / `reason` / `degraded_for_s` / `dropped_rows` / `retry_after_s` |
|
||||
@@ -245,7 +245,7 @@ CHANGELOG 有三处需"请先读这一条"待遇:
|
||||
|---|---|---|
|
||||
| 1 | C 组只读状态快照**要做** | 新增**独立**端口 `TelemetryStatusProvider`(`TelemetryRecorder` 不动,理由见 §3.3);`types.py` 加 `TelemetryStatus`;client 侧一处 `isinstance` 判定。命名避开 `health`(该词在 `ports.py` 已两处占用) |
|
||||
| 2 | D 组(所有权纪律)**一并做** | 改动面从 telemetry 扩到 client/embedding/ocr/backends。拆为**独立前置提交**(纪律统一 + 泄漏修复),验收标准"全套件绿 + 新增用例只在所有权层",该提交即回滚点 |
|
||||
| 3 | 缺省 `POOL_MAX=4` / `WRITE_TIMEOUT=5.0` | 按 §3.5 落 config 校验;README 须给出 `pool_max ≈ 期望吞吐 × RTT` 的调参口径,否则这两个旋钮等于不存在 |
|
||||
| 3 | 缺省 `POOL_MAX=4` / `WRITE_TIMEOUT=5.0` | 按 §3.5 落 config 校验;README 须给出调参口径,否则这两个旋钮等于不存在。**人类当时定的公式 `pool_max ≈ 期望吞吐 × RTT` 已被 §10 修订 #1 作废**(偏乐观一倍),文档一律写实测值 15.6 行/秒 |
|
||||
| 4 | **最低 Python 提到 3.12**,版号定 **1.3.0** | 消解 §6 的 `asyncio.timeout` 版本取舍(可直接用,不退回 `wait_for`)。两项前置(重建环境、UP047 三处改 PEP 695)**已执行完毕并验证**,详见 §7。版号 1.3.0 的依据是缩小支持面,不是新增能力 |
|
||||
|
||||
## 9. 审查留痕(Codex,2026-08-24)
|
||||
@@ -275,3 +275,5 @@ Codex 的取消穿透实测与本会话结论一致(外部 `task.cancel()` 在 `
|
||||
| 1 | **吞吐算术偏乐观一倍**。原稿按 `pool_max / RTT` 估 32 行/秒,T3 实测 50 行并发批 3.2s(≈15.6 行/秒)——`INSERT` 的实际往返比测 RTT 用的 `SELECT 1` 重。方向不变,但下游调参必须拿实测数字 | §3.5 表、§6 两格 |
|
||||
| 2 | **原稿未预见的一处真 bug**: 注入外部池时 `aclose()` 之后的下一次写入会拿 DSN 偷偷自建一个池。与 §1.5 三条同根因,只是表现在关闭之后,T4 修掉 | §1.5 |
|
||||
| 3 | **两条论证被补强**: ①"认不出的失败归行级"这个保守缺省在建池路径上安全,理由是 `min_size=0` 让重试建池零成本(T5);②所有权判定必须用 `is not None` 而非 `or`,否则注入 falsy 后端时自建分支与所有权标志漂移(T1) | §3.2 末、§3.4 |
|
||||
| 4 | **日志级别的决策点收敛到 tracker**(独立验证发现)。原实现在 recorder 的 fatal 分支另发一条 `logger.error`,而 tracker 同时发一条语义重复的 warning——同一个事实两条日志,"级别"这个决策两个源头。改为 `enter_degraded` 按 `fatal` 选级别(error / warning),recorder 不再另发;SQLite 侧的致命档同步升为 error。**这条决策此前没有执法点**: 测试 fixture 挂 `level="WARNING"`,ERROR 与 WARNING 同池,删掉那条 error 用例照样绿。补 `captured_logs` fixture(连级别一起捕获)后三处补上级别断言 | §3.2 表、§3.3 表、§5 单元层 |
|
||||
| 5 | **`acquire` 传的是完整预算,不是剩余预算**(独立验证发现,改文档不改代码): 真正的上界是外层那一层 `asyncio.timeout`,内层再算一次剩余量只是把同一个上界写两遍。行为无害,实测总耗时正好等于预算 | §3.1 |
|
||||
|
||||
Reference in New Issue
Block a user