独立验证发现的 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 各补一句。
7.1 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| design | design:issue15-telemetry-pool-lifecycle | issue #15: 遥测连接池的资源语义与生命周期 | 2026-08-24 |
issue #15: 遥测连接池的资源语义与生命周期
正文: 2026-08-24-issue15-telemetry-pool-lifecycle-design.md。状态: 已实施(2026-08-24,分支 feat/issue-15-telemetry-pool-lifecycle)——人类已确认方案、Codex 已审并逐条处置(正文 §9),T0–T7 全部完成;独立验证发现的 5 个问题已处置,实施期修订见正文 §10。前序: [design:issue9-telemetry-ddl-probe]、design:issue13-schema-mode(schema 单一事实源)。
-
现象: 共享 PG 实例余量紧张时,遥测建池失败 →
_failed永久置位 → 该 client 此后一行遥测都不落库,只有一条 warning,靠人肉对账才发现(19 次调用、成本少记约 $5)。 -
选定方案(四组一次做完): A 池语义(
min_size=0+max_size可配,缺省 4 + 整次写入硬预算 5s);B 失败三分(配置级致命 / 环境级不可用 / 行级拒绝)+ 60s 冷却降级取代永久判死;C 降级可见(共用TelemetryStatusTracker+ 节流复述 + 只读快照TelemetryStatus,走独立端口TelemetryStatusProvider);D 资源所有权纪律统一(谁建的谁关)。 -
地基是一条实测: asyncpg
pool.py:457的if self._minsize:——min_size=0时建池零成本、不触库(实测 0.000s、指向不可达端口照样成功)。这一步把"建池失败"从"混着瞬时错误的一刀切判死"变回真正的确定性失败,于是 issue 提的三个方向里,方向 3(退避重试)大部分不必新建机制(连接失败自动落到acquire,那里本来就是"丢一行、池自恢复"的正确行为),方向 2(共享池)从刚需降级为可选的显式能力(闲时占 0)。 -
判据是主要交付物(两句,经审查补全): ①致命 = 失败原因完全在进程内部且不可变,其余一切失败都可能被外部修好,故一律带冷却重试;②行级 vs 环境级看失败与这一行的数据有没有关系——只与本行数据有关(换一行可能成功)= 行级,与数据无关、每行都会同样失败 = 环境级。按此,致命档窄到只剩"DSN 本身不可解析";认证失败、库不存在、表建不出来、权限被收、表被迁走一律归环境级(修好即自动恢复)。分类按 PG SQLSTATE(切到具体码,非前两位整类)而非 asyncpg 异常类白名单,不随驱动版本漂移。
-
postgres.py:104-105的注释与代码不一致才是病灶: 注释写"池建不出来 = 确定写不进去",这在min_size=10下是假的(连接耗尽只是这一秒写不进去)。冷却重试正面回应了该注释真正的顾虑("每次调用都内联吞一次 connect 超时"): 最坏成本变成"每 60s 一次、上界 5s"。 -
D 组是范围扩展,理由是同一根因的另外三个表现:
GatewayClient.aclose关掉注入的 telemetry(共享 recorder 被第一个关闭的 client 弄死,三处复制)、RedisCache.aclose关掉注入的 redis 客户端、自建的 limiter/breaker redis 客户端从来没人关(泄漏)。不修它,ARCH §7.7 R5 的"共享必须显式注入"这条正道就一直是坏的——缺陷的放大器长在架构里,不在某个默认值里。纪律推广自库内已有的正确先例RedisLimiter._owns_client。 -
被否决备选: 只调默认值(判据错位仍在,下次 PG 重启照样永久失能);只加建池退避(
min_size=0后建池已无可重试的失败);隐式全局池注册表(违反"无全局状态、无模块级单例"铁律);暴露min_size(唯一作用是把脆点装回来);遥测改异步队列 + 后台 flush(真正彻底消除"遥测拖慢业务",但引入进程崩溃丢数窗口,与遥测作为审计证据的定位正面冲突,见 design:issue12-telemetry-retention 决策 E-a);把遥测失败塞进errors.py四分类(那套语义是"决定重试/换源/熔断",遥测不冒泡也不参与,塞进去污染分类)。 -
SQLite 侧有意只做一半: 补可见性(今天初始化失败后写入连 warning 都没有),不做 lazy 化与冷却。它的失败模式(本地目录不可写)在装配期就暴露,不是"跑到一半悄悄断",永久降级语义基本正确;tracker 与快照两侧共用,不产生第二套概念。与 design:issue9-telemetry-ddl-probe 的"两侧有意不对称"同一先例。
-
发布: 版号发布时由人类定(semver 指向 1.3.0,但项目既有口径偏 patch: issue #11 扩端口列落 1.2.1、issue #14 设计写 1.3.0 实际发成 1.2.4)。两处需"请先读这一条"待遇: 遥测常驻连接从
10 × client 数变按需(监控曲线会突变);aclose不再关闭注入的组件(修正越权,但依赖过"注入后由 client 代关"的下游会漏关)。 -
审查留痕(Codex,2026-08-24): 报 3 阻断 + 3 应改 + 1 可选,核实后 6 条采纳、1 条改判为实现约束。三条最重的都是同一类错误——承诺比实现能给的更强: ① "写入墙钟上界 = 一个预算"不成立,
async with pool.acquire()的释放路径是 shielded 且复用 acquire 的 timeout(pool.py:886-889, 930-937),真实上界 ≈ 2 × 预算;②Pool.close()等 in-flight 释放会无限挂,60s 只 warning(pool.py:939-948, 961-972),"关了就是关了"必须自己限时 +terminate();③ 原稿"TelemetryRecorder加health属性零成本"只覆盖静态类型,漏了它是@runtime_checkable(ports.py:246)——加属性会让只实现record_llm_call的对象当场不再满足协议,库内tests/unit/test_ports.py:137,141的 isinstance 断言会红。 -
审查还逼出判据本身的自相矛盾: 原稿只有"致命 = 进程内不可变"一句,却把 SQLSTATE
42整类归了行级——而 42501(权限被收)、42P01(表被迁走)恰恰是"能被外部修好"的。补出第二句判据(行级 vs 环境级看失败与这一行的数据有没有关系),两者改判环境级,42703缺列成为唯一具名例外(它由 design:issue13-schema-mode 的"缺列须逐行暴露"承诺定死)。 -
自查另补两条 Codex 未发现的:
health一词在ports.py已被占用两次(check_health源探活、health(source_name) -> float成功率 EWMA),故快照改名TelemetryStatus(P2 领域术语);asyncio.timeout是 3.11 新增而requires-python = ">=3.11",3.11.0/3.11.1 的uncancel有已知缺陷,实施时须在"抬最低版本"与"改用wait_for"之间选一。 -
最低 Python 提到 3.12(人类决策,2026-08-24): 顺带消解了原 §6 那条取舍(
asyncio.timeout是 3.11 新增、3.11.0/3.11.1 的uncancel有缺陷),现在可直接用、不必退回wait_for。代价有两项且顺序不可颠倒: conda 环境PolyGateway当前是 3.11.15,须先重建;rufftarget-version = "py312"立刻启用 UP047,gather_bounded/_anext_within/stream_with_liveness_timeouts三处要改 PEP 695 语法,而该语法在 3.11 是 SyntaxError——只能在 3.12 环境就位之后改。版号因此确定 1.3.0 起步: 缩小支持面(3.11 下游pip install会被 pip 直接拒绝)比新增能力更该进 minor。