Files
Jyotisha/docs/tasks/TASK-api-chart-subprocess-gate-20261013.md
T
Jesse_ChenandClaude Opus 5.5 2994588ad4 docs(tasks): fold product decisions into audit briefs (BUG-1344..1346)
- dasha-unify: fix Ashtottari birth balance (BUG-1344)
- rect-scoring-perf: server-side candidate cap 241 (BUG-1345)
- node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346)
- consult-prep-parallel: classifier/reserve concurrency declined
- answer-effort-ab: model key supplied by product at acceptance
- drop stricter-than-test line-count red lines (AGENTS.md section 6)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-14 00:41:02 +08:00

22 KiB
Raw Blame History

TASK · 星盘接口与报告的多余子进程、合盘串行、重计算名额轻重混用(issue #待定)

  • 日期:2026-10-13
  • 来源:「2026-10-13 全仓屎山与性能排查」P1 · 星盘接口子进程与闸门
  • 执行方:Grok(Windows + Docker),分支 codex/api-chart-subprocess-gate-20261013,只推这个分支
  • 验收:Claude(Linux)
  • BUG 编号:BUG-1333~1334(开工时核对 docs/BUG_HISTORY.md 最大号;写作时最大号 BUG-1317,BUG-1323~1332 已被同批其它任务书占用)(另:TASK-upstream-sync7-20261014 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
  • 开工前置:TASK-api-disk-cache-retention-20261013(分支 codex/api-disk-cache-retention-20261013)已合入 origin/staging。两单都改 scripts/jyotish_api_server.py,且本单 A/B 依赖那一单的缓存键规范化;未合入不得开工。
  • 主会话补充的串行链(2026-10-14):引擎/API 文件链按此顺序逐单合入后开工(upstream-sync7 也改 jyotish_engine.py / jyotish_api_server.py,若在途,后合一方 rebase):api-disk-cache-retention + report-reference-perf → api-chart-subprocess-gate → pl9-render-split → python-hygiene → dasha-unify → api-server-service-extract。本单的前置以这条链为准(与上一行不一致时以本行为准)。

1. 基线

  • 基线 commit:origin/staging 77057a819(开工时以前置单合入后的 origin/staging 为准,§9 有核对命令)。
  • 生产镜像只装 requirements.txt:有 vedastro==1.23.25,没有 PyJHora(PyJHora 在 requirements-reference-engines.txt,AGPL,只作离线参照)。本单所有计时必须在「模拟生产依赖」下再跑一遍:把只含下面这个 sitecustomize.py 的目录放在 PYTHONPATH 最前(子进程会继承)。
# <临时目录>/nojhora/sitecustomize.py   —— 只挡 jhora;要模拟「无 VedAstro SDK」时把 'vedastro' 也加进集合
import sys
class _Block:
    def find_spec(self, name, path=None, target=None):
        if name.split('.')[0] in {'jhora'}:
            raise ModuleNotFoundError(f"No module named '{name}'")
        return None
sys.meta_path.insert(0, _Block())

2. 事故实证

计时脚本:用 JyotishAPIHandler.__new__(JyotishAPIHandler) 造处理器,直接调 _compute_chart_sync / professional_report_reference.build_professional_report_reference,monkeypatch subprocess.run 记录每次命令与耗时;JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 避开缓存。虚构盘 1990-01-01 12:00 新德里 28.6139/77.2090 +5.5。本机(Linux,无 vedastro SDK、外网关闭)实测:

2.1 /api/chart 不带 skip 每次起 2 个子进程(BUG-1333)

请求 子进程 热态耗时(4 次,后 3 次中位) 响应体
不带 skip_vedastro_main_entry_overview 2 个:vedastro_official_capability_runner.py --bundle official_full_snapshot 与 --bundle official_full_capability_catalog 约 1.03–1.09 s(排查报告另一次测得 0.56 s) 160,643 字节
带 skip 0 约 5–9 ms 55,345 字节
  • 子进程来自 scripts/jyotish_api_server.py::_attach_vedastro_main_entry_overview → vedastro_evidence_orchestrator.orchestrate_vedastro_evidence(route='overview') → scripts/vedastro_service_adapter.py 的 subprocess.run。本机没有 SDK,子进程秒败;生产装了 SDK,若开了外网,这两个子进程会真的打 VedAstro,耗时上限是 VEDASTRO_TIMEOUT_SECONDS——本机测不到,是环境缺口。
  • 出生资料以 --birth-json 明文出现在子进程命令行里(同机 ps 可见)。记录,本单不改。
  • /api/chart 不在 scripts/api_heavy_compute_gate.py::HEAVY_COMPUTE_PATHS 里(tests/test_api_heavy_compute_gate.py::test_health_and_light_paths_are_not_gated 明确断言 "/api/chart" not in HEAVY_COMPUTE_PATHS),所以这 1 s 级的子进程调用不受名额约束。

2.2 前端调用方逐一核对

调用方 是否带 skip 用到响应里的什么 需要 VedAstro 主入口概览吗
frontend/src/lib/chart-view-load.ts(星盘页主盘,payload) 带 盘面 否
同文件 transit 层 → frontend/src/lib/transit-chart-body.ts::transitChartBody 带 行运盘 否
frontend/src/app/api/ephemeris/route.ts(两处) 带 盘面 否
frontend/src/app/api/synastry/route.ts::POST 不带 只读 planets、ascendant、月亮经度(moonLongitude) 否
  • 合盘还串行:selfChart 与 partnerChart 两次 await postPython("/api/chart", …) 先后执行;romance 分支里两次 /api/varga_full(D9)也是串行(business 分支已是 Promise.all)。按本机数字,合盘一次光是两次 chart 就约 2 s,带 skip 并发后约 10 ms 级。
  • Python 内部:execute_consultation_workflow 的 compute_chart 步骤按 defer_optional_external_evidence 传 skip(前台咨询 skip,BUG-161);个人报告、高严谨、主题报告、MCP / API Explorer(API_COMMAND_MAP 的 chart、full-reading)走不 skip 的完整链。这些保持不变(BUG-161:个人报告保持默认完整工作流)。

2.3 报告每次起 10 个子进程(BUG-1334)

/api/professional_report_reference(edition=full_data、languages=["zh","en"],个人报告长文的真实请求形状,见 frontend/src/lib/personal-report-longform-generate.ts),各跑 2 次:

环境 总耗时 子进程数 子进程合计
本机(装了 PyJHora) 19.8 s / 18.4 s 10 6.5 s / 6.0 s
模拟生产(挡 jhora,本机本就无 vedastro SDK) 13.5 s / 13.2 s 11 0.71 s / 0.92 s

子进程明细(模拟生产):

子进程 次数 每次 来源
vedastro_official_capability_runner.py(snapshot / catalog)+ 无 SDK 时多一个 vedastro_python_bridge.py 2–3 0.06–0.09 s 官方快照,报告完整链的一部分,保留
oracle_collection_queue.py → oracle_evidence_validator.py 2 0.03–0.27 s scripts/jyotish_engine.py::_oracle_progress_snapshot:读仓库里的静态文件 references/oracle/dasha_shadbala_oracle_cases.json 做校验,与用户盘无关,每份报告重跑
python -c "from scripts.annual_pyjhora_replay import _isolated_tajaka_worker…" 3 0.06–0.08 s scripts/annual_pyjhora_replay.py::_run_isolated_tajaka_year_lord
python -c "… _isolated_annual_worker…" 3 0.06–0.08 s ::_run_isolated_annual_replay
  • 修正排查报告:生产没有 PyJHora 时这 6 个 PyJHora 子进程确实「快速失败但仍付启动开销」,但每个只有约 60–80 ms,合计约 0.4 s;排查报告里的「年度回放 3 次 ~4.2 s、Tajika 3 次 ~0.9 s」只在装了 PyJHora 的机器(开发机、参照环境)成立。生产上的收益是每份报告约 0.5–0.9 s、8 个子进程,不是 5 s。
  • 另:scripts/tajika.py::_resolve_external_pyjhora_python 不缓存「找不到 PyJHora」的结论,每次都对 python3.14、python3 各起一个 find_spec 探测子进程(当前解释器路径与候选相同时才跳过)。生产镜像里 sys.executable 与 which python3 是否同一路径未核实——执行方在 Docker 镜像里核实并写进 PROGRESS。

2.4 重计算名额轻重混用(只评估)

  • HEAVY_COMPUTE_PATHS 共 22 条,共用一个 BoundedSemaphore(JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2),满了立刻 429(acquire_heavy_compute_slot 非阻塞)。
  • 排查报告数字(本单 A/B 复测):/api/consultation_workflow 卡片缓存命中时约 0.2 s;个人报告约 18 s(本机模拟生产 13 s);校正打分 5–12 s。两份报告同时在算时,任何一条 0.2 s 的咨询请求都会拿到 429(前端 frontend/src/mastra/consultation-workflow.ts 映射为 workflow_rate_limited)。
  • 现在 429 发生时日志里看不出名额被谁占着,staging 上无法从记录确认这种「连带 429」的频率。

3. 根因

  1. BUG-1333:合盘路由写于 BUG-161 之前或之外,没跟上「只读盘面的调用方带 skip」的约定;/api/chart 默认走完整链(为 MCP / 报告保留),于是任何漏带 skip 的调用方都会白付两个 VedAstro 子进程。串行 await 是写法问题。不是回归,是约定从没落到这个路由,也没有合同测试拦。
  2. BUG-1334:PyJHora 回放为了 AGPL 隔离用子进程(正确),但没在父进程先判断「本机根本没装 PyJHora」;oracle 进度快照是给离线审计表用的,被挂在每份报告的技法审计表构建里,又没有进程内复用。
  3. 名额:闸门按「路径」一刀切,不按耗时分级;BUG-717 明令不得靠放宽路径或调高并发解决 429,所以轻重分名额不是技术问题,是产品取舍。

关联:BUG-161、BUG-301(前台仍取官方层)、BUG-715 / BUG-717(星盘页不得 429、不得放宽 HEAVY_COMPUTE_PATHS 或提高并发)、BUG-720(前台不得无上限排队)、BUG-721(禁止模块级缓存跨请求持有出生资料派生数据)、BUG-723(429 必须单独成档)、BUG-1302(预热不占名额)、BUG-1265 / 验收须模拟无 PyJHora。

4. 决策记录

  • 产品负责人 2026-10-13 同意按排查结论全部立单交 Grok 执行;P1,必须 A/B。
  • 授权范围:合盘改带 skip 并发;报告里与用户盘无关或必然失败的子进程在父进程短路;给闸门加观测。
  • 不推翻 BUG-717:本单不把任何路径移出 HEAVY_COMPUTE_PATHS,不新增第二个并发池,不改 JYOTISH_HEAVY_COMPUTE_CONCURRENCY 的语义与默认值。轻重分名额只做评估与模拟(T5),结论交产品拍板后另立单。
  • 不推翻 BUG-161 / BUG-301:/api/chart 不带 skip 时的默认行为(附 VedAstro 主入口概览)不变;报告、高严谨、MCP 的完整链不变。
  • /api/chart 不带 skip 时占名额(T3)是收紧而非放宽,但会让 MCP 等外部调用方可能拿到 429,所以做成开关、默认关,A/B 有证据再提议打开(打开仍需拍板)。

5. 硬红线

  1. scripts/jyotish_api_server.py:行数只按 tests/test_api_server_growth_contract.py 的粗护栏(AGENTS.md §6:不另加比测试更严的行数红线,也不得压行凑数),类方法数与 JyotishAPIHandler.__new__ 伪造点不得增长。闸门的判定逻辑(含 BUG-1302 的 consult_card_warm_only 判断)可以搬进 scripts/api_heavy_compute_gate.py,主文件调用点保持一行。
  2. frontend/src/app/(app)/page.tsx 不碰;不写第二套请求封装。
  3. 不改 deploy/**、.gitea/workflows/**、数据库结构与迁移;不升级依赖。
  4. 不改引擎数值。所有改动在「模拟生产依赖」与「装了 PyJHora」两种环境下,报告 Markdown(中英)与 fact_table_packet 逐字节一致(去掉生成时间戳等已知易变字段,易变字段清单写进差分脚本并在 PROGRESS 列出)。
  5. 进程内复用只允许缓存与用户无关的结论(PyJHora 是否可导入、静态 oracle 文件的校验结果,键含文件路径 + mtime + 大小);不得缓存任何含出生资料的结果(BUG-721)。
  6. tests/test_api_heavy_compute_gate.py 现有断言(含 "/api/chart" not in HEAVY_COMPUTE_PATHS、DEFAULT_CONCURRENCY == 2)一条不改。
  7. 改既有断言写「原值 / 新值 / 原因」三栏;golden 来自真实引擎输出。

6. 任务分解

T1 · 合盘:带 skip、两两并发(BUG-1333)

  • frontend/src/app/api/synastry/route.ts:两次 /api/chart 请求体加 skip_vedastro_main_entry_overview: true(在路由里加,不改 synastryBirthPayload,因为 global-birth-payloads.ts 别处也用);selfChart / partnerChart 改 Promise.all;romance 分支两次 D9 /api/varga_full 改 Promise.all;/api/synastry 调用仍在两者之后。错误语义不变(任一失败仍走现有 catch 返回 blocked)。
  • 新合同测试(前端):桩 fetch,断言两次 /api/chart 请求体都带 skip、两次是并发发出(第二次在第一次 resolve 之前已发出);romance 与 business 两分支的最终 JSON 与改前桩输出一致。
  • 同时加一条源码合同:frontend/src/app/api/** 与 frontend/src/lib/** 里所有向 Python /api/chart 发的请求体都必须带 skip_vedastro_main_entry_overview: true,除非在白名单里写明理由(当前白名单为空)。
  • 验收:tsc --noEmit 0 错、npm run lint 0 error、新测试与 frontend/tests/chart-view-route.test.ts、frontend/tests/ephemeris-route.test.ts 单文件通过。
  • 分工:Grok 自跑;Claude 复验 + 前端全量 npm test。

T2 · 报告子进程短路(BUG-1334)

  • scripts/annual_pyjhora_replay.py:在起子进程前,父进程用 importlib.util.find_spec("jhora") 判断(进程内记一次,可被测试重置)。不可导入时不起子进程,直接返回与「子进程在无 PyJHora 环境下返回的结果」完全相同的 dict。做法:先在模拟生产环境里真实跑一次子进程,把返回值存成 golden(tests/fixtures/…,写明生成命令),短路分支的返回值必须与 golden 相等(request_hash、settings 等按入参计算的字段照常计算)。
  • scripts/tajika.py::_resolve_external_pyjhora_python:结果进程内记一次(含「没找到」)。
  • scripts/jyotish_engine.py::_oracle_progress_snapshot:按 (oracle_file 路径, mtime, 大小) 进程内记一次;文件变了自动重算。
  • 不改:VedAstro 官方快照子进程;装了 PyJHora 时的子进程隔离(AGPL 边界,必须仍是子进程)。
  • 验收:
    • 新测试:无 PyJHora 时 _run_isolated_tajaka_year_lord / _run_isolated_annual_replay 不调用 subprocess.run 且返回值等于 golden;装了 PyJHora(或 find_spec 被桩成可用)时仍调用子进程;_oracle_progress_snapshot 连调两次只跑一次子进程,touch 文件后重跑。
    • 既有 tests/test_report_reader_main.py、tests/test_report_longform_parity.py、tests/test_report_density_facts.py 及 grep -rln "annual_pyjhora_replay\|_oracle_progress_snapshot\|_resolve_external_pyjhora_python" tests 命中的测试全绿。
  • 分工:Grok 自跑;Claude 复验 + 全量 pytest 与基线逐名比对(引擎相关文件有改动)。

T3 · /api/chart 不带 skip 时占重计算名额(开关,默认关)(BUG-1333)

  • scripts/api_heavy_compute_gate.py 新增 acquire_heavy_compute_slot_for_request(path, body)(或同义名):现有按路径的逻辑原样保留;另加:path == "/api/chart" 且 body 不带真值 skip_vedastro_main_entry_overview 且 body 不带 async/enqueue 且开关 JYOTISH_GATE_CHART_OVERVIEW=1 时,占一个名额。BUG-1302 的 consult_card_warm_only 判断一并搬进来。
  • jyotish_api_server.py 调用点改成这一个函数(新逻辑放新模块,主文件只留调用)。
  • HEAVY_COMPUTE_PATHS 不加 /api/chart。
  • 验收:新测试——开关关时任何 /api/chart 都不占名额;开关开时只有不带 skip 的同步请求占名额,带 skip 的、async 的不占;test_warm_consultation_does_not_consume_the_user_slot 等既有测试不改仍绿。
  • 分工:Grok 自跑;Claude 复验。

T4 · 名额观测(BUG-1333 防复发的一部分)

  • api_heavy_compute_gate.py 记录当前占名额的路径(只记路径字符串与开始时间,进程内计数);HeavyComputeBusy 抛出时,服务端打一行结构化日志:被拒路径、当前占用者路径列表与已占秒数。不得记 body、出生资料、用户标识。
  • 若能不增长主文件:/api/health 的返回里加 heavy_compute: {limit, in_use}(只数字);做不到不增长就不做,写进 PROGRESS。
  • 验收:新测试——饱和时日志行含占用者路径、不含 body 字段;释放后计数归零。
  • 分工:Grok 自跑;Claude 复验。

T5 · A/B 验证(P1 必做)

  • 脚本 scripts/research/api_chart_subprocess_ab.py(位置执行方定,写进 PROGRESS)。A = 前置单合入后的 origin/staging(git show <sha>:<path> 导出到临时目录同进程 import),B = 本分支。每组重复 3 次取中位数,两种依赖环境(模拟生产 / 装了 PyJHora)各一遍;Grok 侧若装了 vedastro SDK 且有外网,再加一组「真 SDK」并如实标注。
    1. 合盘:同一对虚构盘,A 的调用序列(两次不带 skip、串行)vs B(两次带 skip、并发),在 Python 侧用线程池模拟并发。度量:总耗时、子进程数、响应字节。正确性:合盘最终 JSON(selfChart/partnerChart 摘要、synastry、relationshipReport)A/B 逐字节一致。
    2. 报告:build_professional_report_reference(真实请求形状),度量总耗时、子进程数与合计耗时;正确性:中英 Markdown 与 fact_table_packet 去易变字段后逐字节一致。
    3. 闸门模拟(评估用,不改生产行为):用真实 api_heavy_compute_gate 模块,按实测耗时用 sleep 模拟混合负载(例:每分钟 2 份报告、20 次咨询 0.2 s、6 次校正打分 8 s,随机种子固定),统计各路径 429 比例。比较方案:O1 现状;O2 轻路径独立小池(违反 BUG-717,只模拟不实现);O3 轻路径有界等待(≤1.5 s,不增并发,只模拟);O4 名额预留(重路径最多占 N-1,只模拟)。输出每个方案下咨询 429 率、报告/校正 429 率、CPU 最高并发数。
  • 目标:合盘 chart 段耗时 B ≤ A 的 10%、子进程 4 → 0;报告子进程 B ≤ 3(模拟生产)、总耗时不劣于 A;所有正确性比对 0 差异。达不到如实写。
  • 开关默认值:T3 默认关;T1、T2 不设开关(纯去冗余,正确性由逐字节比对保证)。
  • 记录写 docs/testing/api-chart-subprocess-gate-ab-20261013.md;O1–O4 的模拟结果整理成给产品看的一张表(方案 / 咨询 429 率 / 报告 429 率 / 是否触碰 BUG-717)。
  • 分工:Grok 跑并交记录;Claude 在 Linux 上复跑 1、2 两项(数字差 >20% 以 Claude 为准)。

T6 · 记录

  • docs/BUG_HISTORY.md:BUG-1333(/api/chart 漏带 skip 的合盘白付 VedAstro 子进程且串行、不占名额、429 无观测);BUG-1334(报告每份 10 个子进程,其中 PyJHora 回放在生产必然失败、oracle 静态校验每次重跑)。BUG-1334 的「验证」里写清生产收益是约 0.5–0.9 s / 份,不是 5 s。
  • docs/tasks/PROGRESS-api-chart-subprocess-gate-20261013.md,含 T5 O1–O4 结论与「需拍板」一节。
  • CHANGELOG.md:「合盘更快」一句。
  • docs/tasks/README.md 索引加本单。
  • 不改 frontend/DESIGN.md(无 UI 变化)。

7. 让步顺序

时间不够时按此顺序往后砍:T4 的 /api/health 字段 → T3 → T5.3 闸门模拟 → T2 的 _resolve_external_pyjhora_python。T1、T2 主体、T5.1–5.2、T6 不可砍。

8. 验收命令分工

谁 命令
Grok 自跑 .venv 下:python -m pytest tests/test_api_heavy_compute_gate.py tests/test_api_server_growth_contract.py tests/test_report_reader_main.py tests/test_report_longform_parity.py tests/test_report_density_facts.py <T2/T3/T4 新测试> -q;前端 tsc --noEmit、npm run lint、T1 新测试与 chart-view-route / ephemeris-route 单文件;T5 脚本。前端全量、Python 快速门、全量 pytest 不跑,以 Claude 为准
Claude 验收 上述全部 + run_quality_gate.py --profile quick + 全量 pytest 与基线逐名比对(T2 动了引擎文件)+ 前端全量 npm test + 在挡 jhora 的环境复跑 T2 测试与 T5.1–5.2 + wc -l scripts/jyotish_api_server.py 不超过开工值
不需要 test:db;next build 只在 Claude 侧跑一次确认 / 仍 Static(只改了 API 路由)

9. 开工前置命令

git fetch origin --prune
# 前置单必须已合入:
git log origin/staging --oneline | grep -i "api-disk-cache-retention\|BUG-1330" | head -3   # 必须有结果
git merge-base --is-ancestor origin/codex/api-disk-cache-retention-20261013 origin/staging && echo merged
git worktree add -b codex/api-chart-subprocess-gate-20261013 .worktrees/api-chart-subprocess-gate-20261013 origin/staging
cd .worktrees/api-chart-subprocess-gate-20261013
git log -1 --format=%h
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3      # 确认 1333、1334 未被占
wc -l scripts/jyotish_api_server.py                   # 记下开工行数
grep -rn "skip_vedastro_main_entry_overview\|\"/api/chart\"" frontend/src --include=*.ts --include=*.tsx
grep -rln "annual_pyjhora_replay\|_oracle_progress_snapshot\|_resolve_external_pyjhora_python\|acquire_heavy_compute_slot" scripts tests
# Docker 镜像内核实(写进 PROGRESS):
#   python -c "import sys,shutil;print(sys.executable, shutil.which('python3'), shutil.which('python3.14'))"
#   python -c "import importlib.util as u;print(u.find_spec('jhora'))"   # 预期 None

10. 环境缺口(预告)

  • 生产装了 vedastro SDK:不带 skip 的 /api/chart 在开外网时的真实耗时(上限 VEDASTRO_TIMEOUT_SECONDS)本机测不到;合盘改动部署 staging 后,由 Claude 用虚构盘在内网复测一次合盘耗时,或写进 docs/testing/ 清单交产品负责人在页面上计时。
  • 闸门「连带 429」的真实频率:T4 上线后看 staging 日志一周再给产品结论。
  • 轻重分名额方案(O2/O3/O4):需产品拍板,本单不实施。