Files
PolyGateway/research-wiki/designs/issue15-telemetry-pool-lifecycle.md
T
iomgaa 6d6b3cf59c 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 各补一句。
2026-08-24 11:55:22 -04:00

7.1 KiB
Raw Permalink Blame History

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:457if 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();③ 原稿"TelemetryRecorderhealth 属性零成本"只覆盖静态类型,漏了它是 @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,须先重建;ruff target-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。