perf(consultation): 健康探测加 TTL、yoga 表达式只编译一次
BUG-734: probe_official_rest_health 不是 ping —— 它把一份虚构 smoke 排盘 POST 给官方 HoroscopePredictions(实测单次 1,034 ms),还经 call() 吃掉对方 每分钟 5 个限流令牌之一,而结论零 TTL。连续 6 轮前台请求下,第 6 次探测会被 本地限流器挡下、反过来报出假的 official_blocked,真正的业务调用还要和它抢令牌。 加进程级 TTL 缓存(成功 60 s / 失败 10 s,配置变即失效,缓存键只记 key 配没配 的布尔)。force_refresh 默认 True:诊断端点调的是裸 gateway_status() 且该文件 本轮不得改,默认实时才不会让运维看到旧结论;run_gateway_packet 显式走 False。 探测在锁外执行。6 轮实测:探测 6→1、握手 5→1、限流令牌 6→1。 连接复用(任务书 6.2)按让步顺序砍掉:实测 urllib.request 没有连接池,模块级 共享 opener 在 6 次请求下仍开 6 条 TCP 连接,按任务书写法只能骗过「同一个 opener 实例」的断言而省不掉任何握手。另立单。 BUG-735: yoga_engine._eval_custom 对静态规则表的表达式每请求重新编译。实测每次 检测 386 次源码编译(182 次 eval 字符串 + 204 次 ast.parse/compile)。拆出 _build_custom_exec_globals(每次新建,绑定当前盘 ctx)与 lru_cache 的 _compiled_eval_code / _compiled_exec_code,只缓存 code object,绝不缓存求值 结果。求值语义逐条对齐改前。第二次检测起编译 0 次,单次 27.4 ms → 3.3 ms。 等价用同进程差分(BUG-733 的做法):192 条去重表达式 × 3 张公开虚构示例盘, 与逐字复刻的改前实现同值,另有反向验证证明差分非恒真。 未改 scripts/jyotish_api_server.py。既有断言一条未改。新增 23 条测试。 快速门唯一红的 test_chat_page_uses_authenticated_cloud_persistence 经干净 origin/staging 检出复跑确认为基线红,记入 BLOCKED.md BLK-002,不顺手修。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
co-authored by
Claude Opus 5
parent
10afdcddeb
commit
63c419f375
@@ -11412,3 +11412,35 @@
|
||||
- 相关记录:BUG-712、BUG-721
|
||||
- 复发自:BUG-712
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-734 | 每轮聊天用一次完整业务请求做健康探测,吃光对方限流额度还不复用连接
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-16
|
||||
- 最近更新:2026-09-16
|
||||
- 影响面:`scripts/vedastro_gateway.py` 的 `probe_official_rest_health` / `gateway_status` / `run_gateway_packet`;经 `run_gateway_packet` 的全部前台路径(`professional_reading`、`rectification_gate`、`/vedastro/gateway/run`)
|
||||
- 用户现象:BUG-727 的快照缓存每轮都命中、响应体从 52 万字符降到 40 万,耗时却几乎没变。逐轮 trace 显示快照命中的轮次仍然多打一次外网,每轮多等约 1 秒。连续提问到第 6 轮时技法表的 VedAstro 云状态会毫无道理地变成 blocked。
|
||||
- 触发条件:任何经 `run_gateway_packet` 的前台请求;同一分钟内连续 6 轮即可复现假 `official_blocked`。
|
||||
- 根因:三件事叠在一起。(1)`probe_official_rest_health` 不是 ping——它 POST 一份虚构 smoke 排盘到官方 `HoroscopePredictions`,拿返回的 Status 当健康信号,本机实测单次 1,034 ms;(2)它经 `call()` 且没传 `skip_rate_limit`,每轮都吃掉对方每分钟 5 个限流令牌之一,于是第 6 轮探测被本地限流器挡下、反过来报告服务不可用,真正的业务调用还要和它抢令牌;(3)探测结论没有任何 TTL,而「这个外部服务此刻可用吗」不会每 500 毫秒变一次。这是 BUG-721 同一形状的又一个实例:只依赖静态输入的昂贵结果被放在每请求都走的路径上。BUG-727 只覆盖了快照,没覆盖探测。
|
||||
- 修复:探测主体逻辑一字未改地挪进 `_probe_official_rest_health_uncached()`;`probe_official_rest_health` / `gateway_status` 增加 `force_refresh` 形参并加进程级 TTL 缓存,`run_gateway_packet` 显式走 `force_refresh=False`。缓存键是影响探测结论的有效配置(mode、official endpoint、self-host endpoint、network enabled、是否配了 API key、`JYOTISH_SKIP_LOCAL_ENV`),配置一变立刻失效;只记 key 配没配的布尔,绝不存 key 的值。成功 TTL 60 秒、失败 TTL 10 秒(失败必须显著更短,否则对方恢复了还要再瞎等一分钟)。探测在锁外执行,不在等外网时按住进程级锁。`force_refresh` 默认 `True`:诊断端点 `_compute_vedastro_gateway_status` 调的是裸 `gateway_status()` 且该文件本轮不得改(AGENTS §6),默认实时才能保证运维不会看到一个 60 秒前的假象。连接复用这一半未做——实测证明 `urllib.request` 没有连接池,模块级共享 opener 在 6 次请求下仍开 6 条 TCP 连接(`AbstractHTTPHandler.do_open` 每次新建并强制发 `Connection: close`),按任务书设想实现只能通过「是同一个 opener 实例」的断言而省不掉任何握手;真正的连接池需要改所有业务调用共用的 `call()` 或新增依赖,另立单。
|
||||
- 验证:`tests/test_vedastro_health_probe_cache.py` 14 条——连调 3 次只探测 1 次、推过 TTL 后变 2 次、失败 TTL 过后成功 TTL 之内服务恢复能被看见、失败 TTL < 成功 TTL、配 API key 或换 endpoint 立刻失效、缓存键不含 key 值、network disabled 不碰外网、`force_refresh=True` 每次真探测且默认值被 `inspect.signature` 钉死、`run_gateway_packet` 必须传 `force_refresh=False`、返回值被调用方改写不污染缓存、探测期间锁可获取。同口径实测(连续 6 轮前台请求):探测 6 → 1 次,TLS 握手 5 → 1 次,消耗对方限流令牌 6 → 1 个;改前第 6 轮健康结论为假 `official_blocked`,改后 6 轮全部 `official_verified`。既有 `tests/test_vedastro_gateway.py`、`tests/test_vedastro_runtime_ops.py` 断言一条未改仍绿。
|
||||
- 防复发:前台路径上的外部服务探测必须有 TTL,且不得消耗业务限流额度;失败 TTL 必须显著短于成功 TTL;诊断端点必须保持实时,缓存只能加在前台那条路上。健康探测不得用一次完整业务调用充当。探测与业务调用共用连接这一条仍未落地——在引入带连接池的客户端之前,每次外呼仍各付一次 TLS 握手(本机实测 207 ms)。
|
||||
- 相关记录:BUG-727(快照缓存,只修了快照没覆盖探测)、BUG-161、BUG-301(前台外部证据的两条既有红线)、BUG-721(同一形状)、BUG-718(不得持锁等外网)
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-735 | 静态规则表达式每个请求重新编译 386 次
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-16
|
||||
- 最近更新:2026-09-16
|
||||
- 影响面:`scripts/yoga_engine.py` 的 `_eval_custom`;所有走 yoga 判定的路径(普通咨询、报告、校正打分)
|
||||
- 用户现象:用户看不见错误结果,只感到慢。单请求 cProfile 里运行时 AST 编译占 9.4%,而真正的占星计算 `swisseph.calc_ut` 只占 2.4%。
|
||||
- 触发条件:任何一次 yoga 检测。
|
||||
- 根因:`_eval_custom` 拿到的是源码字符串 `cond["expr"]`,每次调用都重新编译:先 `eval(expr)`(CPython 在内部编译字符串),多语句表达式在这一步抛 `SyntaxError`、被捕获后再 `ast.parse` → 改写末尾表达式 → `compile` → `exec`。表达式全部来自仓库内的静态规则表 `references/yoga_rules.json`(198 条 `expr`、去重 192 条、最长 2,614 字符、其中 103 条是多语句),在进程生命周期内不会变。本机实测每次检测 386 次源码编译(182 次 `eval` 字符串 + 204 次 `builtins.compile`)。这是 BUG-721 / BUG-734 的同一形状:只依赖静态输入的昂贵结果被放在每请求路径上。
|
||||
- 修复:`_eval_custom` 拆成「造求值命名空间」与「求值」两段。`_build_custom_exec_globals` 每次调用都新建 `exec_globals`(它绑定当前这张盘的 `ctx`)。新增模块级 `_compiled_eval_code` / `_compiled_exec_code`,各带 `functools.lru_cache(maxsize=512)`,**缓存的是 code object,不是求值结果**;`_run_custom_expr` 不带任何缓存装饰器。语义与改前逐条对齐:先表达式模式、编译期 `SyntaxError` 退到改写后的 exec 模式、两条路异常仍全吞结果为 `None`、`ast.parse` 仍对 `expr.strip()`、运行期 `SyntaxError` 也仍退到 exec 分支。不改任何 yoga 规则内容、判定口径或置信度边界。
|
||||
- 验证:`tests/test_yoga_expr_compile_cache.py` 9 条。等价用同进程差分(BUG-733 立下的做法,不写跨机 golden):参照实现逐字复刻改前代码,规则表全部 192 条去重表达式 × 3 张公开虚构示例盘 = 576 次逐条同值,并有反向验证证明该差分不是恒真。红线断言:缓存里是 `types.CodeType`;同一条 `house_of('Sun')` 在两张盘上必须得到 10 与 5 两个不同答案(表达式与多语句两条路都验);`_run_custom_expr` 无 `cache_info`;`maxsize` 有界且 ≥ 去重表达式数。来源合同:规则表在仓库内、全仓 `cond.get("expr"` 仅 1 个读取点、生产入口 `jyotish_engine.py` / `jyotish_api_server.py` 的 `detect_yogas(` 不传 `rules_path`——未发现任何用户输入能进入 `expr`。同口径实测:每次检测源码编译 386 → 第二次起 0,稳态单次检测 27.4 ms → 3.3 ms,命中 yoga 数三轮均为 70 不变。`tests/test_yoga_rules_integrity.py`、`tests/test_yoga_benchmark_cases.py`、`tests/test_lakshmi_yoga.py` 仍绿。
|
||||
- 防复发:`eval` / `exec` 的入参若来自静态规则表,必须缓存编译产物;缓存 code object,**永远不缓存求值结果**——缓存结果会让所有人拿到同一张盘的 yoga 判定,后果比性能问题严重得多。编译缓存必须有界,且必须有断言证明 `expr` 只来自仓库内的静态规则表;若哪天有用户输入能进入 `expr`,立刻停手按安全问题处理。
|
||||
- 相关记录:BUG-721(同一形状的第三个实例)、BUG-734(同一轮的另一处)、BUG-733(同进程差分怎么写)
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
Reference in New Issue
Block a user