- 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
226 lines
26 KiB
Markdown
226 lines
26 KiB
Markdown
# TASK · API 磁盘缓存只进不出:清理、留存期与缓存键规范化(issue #待定)
|
||
|
||
- 日期:2026-10-13
|
||
- 来源:「2026-10-13 全仓屎山与性能排查」P1 · 磁盘缓存与留存(chart 缓存不清理、VedAstro 快照与 artifact 留存出生资料、缓存键碎片)
|
||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/api-disk-cache-retention-20261013`,只推这个分支
|
||
- 验收:Claude(Linux)
|
||
- BUG 编号:**BUG-1330~1332**(开工时核对 `docs/BUG_HISTORY.md` 最大号;写作时最大号 BUG-1317,BUG-1323~1329 已被同批其它任务书占用)(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
|
||
- 开工前置:**无**(第一批,立即派发)。注意:`TASK-api-chart-subprocess-gate-20261013` 也改 `scripts/jyotish_api_server.py`,排在本单**合入 staging 之后**开工。
|
||
- **主会话补充的串行链(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`
|
||
- 生产 API 容器把整个 `scratch/local` 挂在命名卷 `api_scratch`(`deploy/docker-compose.server.yml`),**部署不清空**;下面列的每个目录都会跨部署一直累积。
|
||
- 本机复现环境:系统 `python3`,装了 swisseph、PyJHora,**没有** vedastro SDK,外网关闭。模拟生产依赖(无 PyJHora)用一个只含 `sitecustomize.py` 的目录挡掉 `jhora`(与 `vedastro`)导入,放到 `PYTHONPATH` 最前(子进程会继承):
|
||
|
||
```python
|
||
# <临时目录>/nojhora/sitecustomize.py
|
||
import sys
|
||
class _Block:
|
||
def find_spec(self, name, path=None, target=None):
|
||
if name.split('.')[0] in {'jhora', 'vedastro'}:
|
||
raise ModuleNotFoundError(f"No module named '{name}'")
|
||
return None
|
||
sys.meta_path.insert(0, _Block())
|
||
```
|
||
|
||
## 2. 事故实证(`77057a819`,虚构盘 1990-01-01 12:00 新德里 28.6139/77.2090 +5.5)
|
||
|
||
### 2.1 chart 响应缓存从不清理(BUG-1330)
|
||
|
||
| 事实 | 位置 |
|
||
| --- | --- |
|
||
| chart 缓存写在 `scratch/local/api_chart_cache/<sha256>.json`,记录只有 `cache_key / created_at / payload`,**没有 `expires_at_unix`** | `scripts/jyotish_api_server.py::_api_chart_cache_dir`、`::_store_api_chart_response_cache` |
|
||
| TTL(`JYOTISH_API_CHART_CACHE_TTL_SECONDS`,默认 900 s)只在**读**时比较,过期文件不删 | `::_load_api_chart_response_cache` |
|
||
| 清理函数遍历 `_async_job_dir(_API_CHART_CACHE_SCOPE)` = `scratch/local/api_chart_response_jobs/`(chart **异步任务**目录),按 `expires_at_unix` 删;`api_chart_cache/` 一次都没扫过 | `::prune_expired_async_jobs`、`::_async_job_dir` |
|
||
| 清理只在进程启动时跑一次;长时间不重启的容器里,到期的异步任务记录(含完整盘面结果)只在被轮询时才删 | `::start_server` 调 `prune_expired_async_jobs()` |
|
||
| 缓存键含 `today`,所以同一人每天一个新文件;带 VedAstro 概览的响应约 **160 KB**,带 `skip_vedastro_main_entry_overview` 约 **55 KB**(本机实测 `json.dumps` 字节数) | `::_build_api_chart_cache_payload` |
|
||
| 缓存文件按进程默认权限写(本机 0644),里面是完整出生资料与盘面 | 同上 |
|
||
|
||
### 2.2 缓存键用请求原值,同一张盘多份(BUG-1330)
|
||
|
||
`_build_api_chart_cache_payload` 直接把 `body.get('hour')` 等原值放进键。实测(复现命令见 §9):
|
||
|
||
| 输入变体 | 生成的不同键数 |
|
||
| --- | --- |
|
||
| `hour` = `12` / `12.0` / `"12"` | **3** |
|
||
| 基准 / `tz="5.5"` / 显式 `second=0` / `lat="28.6139"` / `ayanamsa="LAHIRI"` | **4**(`ayanamsa` 已经被 `_request_ayanamsa` 小写,另三种各成一键) |
|
||
|
||
计算侧对这些变体走 `_get_int` / `_get_float` / `_parse_timezone`,结果相同(T3 需用字节比对证实),缓存却各存一份、各 miss 一次。
|
||
|
||
### 2.3 VedAstro 官方快照的 artifact 与语义缓存(BUG-1331)
|
||
|
||
| 事实 | 位置 |
|
||
| --- | --- |
|
||
| 每次成功归一(含 `status=partial`)都写一份**缩进**的 `official_full_snapshot-<req12>-<resp12>.json` 到 `scratch/local/vedastro_adapter/`,无任何清理。本机无 SDK、外网关闭时一次 `/api/chart`(不带 skip)照样写出 **48–52 KB** 的文件,`request_manifest.requests[].body` 里有 `StdTime "12:00 01/01/1990 +05:30"` 与经纬度,文件权限 0644 | `scripts/vedastro_service_adapter.py::_write_artifact`(调用点:`::_normalize_success`、`::_normalize_range_scan_success`、`::_merge_range_scan_reports`、`::_normalize_official_full_snapshot_success`) |
|
||
| 文件名按「请求哈希 + 响应哈希」,同盘同日覆盖;不同人、不同日期都是新文件 | `::_artifact_path` |
|
||
| 读这些文件的只有离线工具与诊断:`::list_official_full_snapshot_artifacts`、`scripts/three_engine_parity_runner.py`(`VEDASTRO_ARTIFACT_DIR`)、`jyotish_api_server.py` 运行状态里的 `latest_artifact`;用户请求链路不读 | grep `ARTIFACT_DIR` / `artifact_path` |
|
||
| 语义缓存 `official_full_snapshot_cache/`:`run_official_full_snapshot_for_case` 对**任何 dict 结果**(成功、partial、blocked)都调 `_store_official_full_snapshot_semantic_cache`,TTL>0 即落盘 | `::run_official_full_snapshot_for_case` |
|
||
| TTL 取 `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS`,**没设时回落到 `VEDASTRO_CACHE_TTL_SECONDS`**,都没设为 0(不缓存)。`.env.cn.example` 两个都写 `604800`(7 天)→ 按样例部署时,一次失败的快照会被原样复用 7 天 | `::_official_full_snapshot_cache_ttl_seconds`、`.env.cn.example` 13–14 行 |
|
||
| 诊断接口报出的 `official_full_snapshot_cache_ttl_seconds` 只读自己的变量,**不含回落**,与实际生效值可能不一致 | `scripts/vedastro_gateway.py::gateway_status`(`_int_env("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", 0)`) |
|
||
| 业务响应缓存 `vedastro_adapter/response_cache/`:默认 TTL 86400 s,同样只在读时判断、不删 | `::_load_cached_payload`、`::_store_cached_payload` |
|
||
| 生产 `.env.production` 里这几个变量的实际值本单读不到(密钥文件,不得打印) | 环境缺口,见 §10 |
|
||
|
||
### 2.4 其它只进不出的目录(BUG-1332)
|
||
|
||
| 目录 | 写入方 | 现状 |
|
||
| --- | --- | --- |
|
||
| `scratch/local/vedastro_snapshot_cache/` | `scripts/vedastro_snapshot_cache.py::store_snapshot`(BUG-727) | 键含 UTC 参考日;`lookup_snapshot` 最多回看 `MAX_STALE_DAYS=7` 天,**更早的文件永远不会再被读,但从不删**。每人每天一个 |
|
||
| `scratch/local/vedastro_snapshot_negative_cache/` | `scripts/vedastro_consultation_snapshot.py::store_negative_snapshot`(BUG-1231,10-05 遗留) | 只认当天(`lookup_negative_snapshot` 比对 `reference_date`),**昨天及以前的全是死文件**,每人每天一个,无清理 |
|
||
| `scratch/local/vedastro_gateway_jobs/` | `scripts/vedastro_gateway.py::_write_job` / `::_raw_response_archive` | 仅队列模式写;无清理;job 文件含 `request.case` 出生资料,缩进写、默认权限(原始响应存档已是 0600) |
|
||
| `scratch/local/varga_segments/` | `scripts/rectification_varga_segments_api.py` | 按校正窗口一份,无清理 |
|
||
| `scratch/local/high_rigor_jobs/`、`api_chart_response_jobs/` | 异步任务 | 有 `expires_at_unix`,但只在启动时清(见 2.1) |
|
||
| `scratch/local/consult_full_data_cache/` | `scripts/consult_full_data_cache.py` | **已有**两天清理(`prune_cache`,每次 `get_or_build` 都 glob+stat 全目录)。本单只记录,见 T6 |
|
||
| `$TMPDIR/jyotisha-reports/` | `jyotish_api_server.py` 报告 HTML/PDF 产物(`REPORT_ARTIFACT_DIR`) | 不在命名卷里,容器重建即清;容器存活期间无清理。本单只记录 |
|
||
|
||
### 2.5 咨询卡片缓存体积(只评估)
|
||
|
||
虚构盘、模拟生产依赖,`consult_full_data_cache.get_or_build` 实测:未命中构建 1.9 s;文件 **5,097,751 字节**;命中时 `json.loads` 中位 **68.5 ms**,整次 `get_or_build` 命中 **82.5 ms**(含 `prune_cache` 全目录 stat)。每人每天一份、保留两天 ≈ 每个活跃用户 10 MB。
|
||
|
||
## 3. 根因
|
||
|
||
1. **chart 缓存(BUG-1330)**:写缓存的人给了读时 TTL,写清理函数的人以为 chart 缓存和 chart 异步任务在同一个目录、同一种记录格式;两者从未对上。清理只挂在启动钩子上。不是回归,是从一开始就漏设计。键不规范化同理:键直接取 `body` 原值,而计算侧会先解析成数字。
|
||
2. **VedAstro artifact(BUG-1331)**:`_write_artifact` 是早期为「外部 oracle 证据可审计」写的落盘,当时只在离线脚本里跑;后来同一归一函数进了 `/api/chart`、报告、校正等在线路径,落盘没有跟着关掉。快照语义缓存把「拿到了一个 dict」当成「拿到了可复用的结果」,没有区分失败。BUG-734 已确立「失败 TTL 必须显著短于成功 TTL」,这里没跟上。
|
||
3. **前台快照缓存与负缓存(BUG-1332)**:BUG-727 / BUG-1231 只设计了「何时还能读」,没有设计「何时可以删」。
|
||
4. 所有这些目录在生产都是持久卷,部署不会顺带清空,所以问题只会随时间变大。
|
||
|
||
关联:BUG-161(快速排盘与完整证据排盘必须不同缓存键)、BUG-301、BUG-727、BUG-734、BUG-1231、BUG-721(禁止模块级缓存跨请求持有出生资料派生数据)、BUG-994(不得用 `volume prune` 换磁盘)。
|
||
|
||
## 4. 决策记录
|
||
|
||
- 产品负责人 2026-10-13 同意按排查结论全部立单交 Grok 执行;本单为 P1,按「P1/P2 必须 A/B」要求执行。
|
||
- **artifact 默认不写**:`official_full_snapshot-*.json` 等审计落盘改为开关,默认关;需要做外部引擎比对时由离线脚本显式打开。依据 AGENTS.md §8「真实用户出生资料不得进入……任何提交」的精神扩展到服务器磁盘:在线请求不留存非必要的出生资料副本。
|
||
- **失败快照短 TTL**:沿用 BUG-734 的口径(失败 TTL 显著短于成功 TTL)。
|
||
- **不推翻任何既有红线**:
|
||
- BUG-161:skip 与不 skip 的 chart 请求仍是不同缓存键(规范化不得合并这两类)。
|
||
- BUG-727 / BUG-1231:`vedastro_snapshot_cache.snapshot_cache_key` 的键材料**本单不改**(BUG-1231 写明「整数小时和规范后的浮点小时靠原始请求对齐」,改键会打断这条对齐);本单只给它加「删过期文件」。
|
||
- BUG-301:前台仍取官方层;本单不改任何取数路径与等待预算。
|
||
- BUG-721:不得为省 T6 的 80 ms 加模块级/进程级的已解析数据包缓存(那是跨请求持有出生资料派生数据)。
|
||
- **本单不做,需拍板**:改 `deploy/docker-compose*.yml` 的卷配置、给卷设容量上限、改生产 `.env.production` 里的 TTL 值、手工清理生产卷上的存量文件(上线后由新清理逻辑自然清掉;若产品要立刻清,另行拍板并由有权限的人执行,不用 `volume prune`)。
|
||
|
||
## 5. 硬红线
|
||
|
||
1. `scripts/jyotish_api_server.py`:行数只按 `tests/test_api_server_growth_contract.py` 的粗护栏(AGENTS.md §6:不另加比测试更严的行数红线,也不得压行凑数),类方法数与 `JyotishAPIHandler.__new__` 伪造点不得增长。新逻辑全部进新模块(建议 `scripts/api_scratch_retention.py`),主文件只做薄调用;`prune_expired_async_jobs` 的函数体可以整体搬进新模块,主文件留同名薄包装以保住 `tests/test_api_async_job_contract.py` 的调用入口。
|
||
2. 不新增 `JyotishAPIHandler` 类方法;不新增端点。
|
||
3. 不改 `deploy/**`、`.gitea/workflows/**`、数据库结构与迁移。
|
||
4. 不改任何引擎数值、盘面结果、证据取舍;chart 接口在规范化前后**响应逐字节一致**(`runtime_cache` 字段除外)。
|
||
5. 清理只删**按现有读逻辑已经不可能再被读到**的文件,或本单新定义的留存期已过的文件;不删未知格式文件中 mtime 在 24 小时内的;任何清理异常只计数、不抛、不阻塞请求。
|
||
6. 清理不得在请求线程里同步扫大目录:周期清理走后台线程或「距上次清理 ≥ N 秒才跑」的节流,单次耗时写进返回计数。
|
||
7. 不顺手升级依赖、不修任务书外的 warning;改既有断言写「原值 / 新值 / 原因」三栏。
|
||
8. 测试与 A/B 只用虚构或公开名人盘;A/B 记录里不得出现生产路径下的真实文件名、哈希以外的出生资料。
|
||
|
||
## 6. 任务分解
|
||
|
||
### T1 · 统一的 scratch 留存模块 + chart 缓存清理(BUG-1330)
|
||
|
||
- 新模块 `scripts/api_scratch_retention.py`:
|
||
- 一张登记表,每项 = 目录 + 判定「可删」的规则 + 是否含出生资料。至少登记:`api_chart_cache`(`created_at + chart TTL` 已过)、`high_rigor_jobs` 与 `api_chart_response_jobs`(`expires_at_unix` 已过,即现有逻辑)、T2/T4 的各目录。
|
||
- `prune_all(now=None) -> dict`:逐目录返回 `scanned / removed / errors / elapsed_ms`,只计数不打印文件名。
|
||
- 节流的周期清理:`start_periodic_prune(interval_s)`,后台守护线程,默认间隔 `JYOTISH_SCRATCH_PRUNE_INTERVAL_SECONDS=1800`,`0` 关闭周期清理(启动清理仍跑)。
|
||
- `jyotish_api_server.py`:`start_server` 里把 `prune_expired_async_jobs()` 换成新模块的启动清理 + 启动周期线程(保持打印一行汇总的行为);`prune_expired_async_jobs` 函数体搬走,留薄包装。新逻辑放新模块,主文件只留调用。
|
||
- chart 缓存新写入的文件改 0600、改为临时文件 + `os.replace` 原子写(与 `_write_async_job_record` 一致)。
|
||
- 不改:TTL 默认值、读缓存逻辑、`runtime_cache` 字段。
|
||
- 验收标准:
|
||
- 新测试 `tests/test_api_scratch_retention.py`:在 `tmp_path` 下造三类 chart 缓存文件(未过期 / 已过期 / 坏 JSON 且 mtime 超 24 h),`prune_all` 只删后两者;异步任务目录行为与现有 `test_startup_cleanup_removes_only_expired_job_records` 一致;周期线程在 `interval=0` 时不启动;清理抛错时返回 `errors>0` 不抛。
|
||
- `tests/test_api_async_job_contract.py`、`tests/test_api_server_security.py -k "chart_cache or chart_response_cache"`、`tests/test_api_server_growth_contract.py` 全绿。
|
||
- 分工:Grok 自跑以上;Claude 复验 + 快速门。
|
||
|
||
### T2 · VedAstro artifact 默认不写、快照缓存失败短 TTL、TTL 诊断如实(BUG-1331)
|
||
|
||
- `scripts/vedastro_service_adapter.py::_write_artifact`:新开关 `VEDASTRO_WRITE_AUDIT_ARTIFACTS`(默认 `0`)。关时不落盘,`source_metadata.artifact_path` 写 `None`(字段保留,值为空;先 grep 所有读 `artifact_path` 的地方确认对 `None` 安全:`scripts/vedastro_user_entrypoint.py`、`scripts/diagnose_external_engine_adapters.py`、`scripts/three_engine_parity_runner.py`、`jyotish_api_server.py` 的 `latest_artifact`)。开时照旧写,但改为紧凑 JSON、0600、原子写。
|
||
- `run_official_full_snapshot_for_case`:只有「成功」结果按正常 TTL 缓存;失败 / partial / blocked 用短 TTL `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_NEGATIVE_TTL_SECONDS`(默认 300,且强制 ≤ 正常 TTL)。「成功」的判据由执行方读 `_normalize_official_full_snapshot_success` 与 `_run_official_full_snapshot_case` 的返回形状确定后写进 PROGRESS(预期:`available is True` 且 `status` 为成功态)。语义缓存记录增加 `negative: true/false`,读时按对应 TTL 判断。语义缓存文件改 0600、原子写。
|
||
- `scripts/vedastro_gateway.py::gateway_status` 的 `official_full_snapshot_cache_ttl_seconds` 改为调用 adapter 的 `_official_full_snapshot_cache_ttl_seconds()`(含回落),另加 `official_full_snapshot_negative_cache_ttl_seconds`。
|
||
- 留存:把 `vedastro_adapter/official_full_snapshot_cache/`、`vedastro_adapter/response_cache/`(按各自 `created_at + TTL`)、`vedastro_adapter/*.json` artifact(开关关时:mtime 超 24 h 即删;开关开时:保留 `VEDASTRO_AUDIT_ARTIFACT_KEEP_DAYS`,默认 7)登记进 T1 的表。
|
||
- `.env.cn.example`:在两个 604800 旁加注释说明「失败结果另有短 TTL;含出生资料,7 天为上限」——**不改数值**(是否把生产值调短,需拍板)。
|
||
- 不改:快照的取数、并发、超时、`official_closure_state` 判定。
|
||
- 验收标准:
|
||
- 新测试:开关默认关时,一次 `_normalize_official_full_snapshot_success`(用现有测试里的 fixture)不产生任何文件、`artifact_path is None`;开关开时产生 0600 文件;失败结果在 TTL=604800 下 301 s 后不再命中、成功结果仍命中;`gateway_status` 在只设 `VEDASTRO_CACHE_TTL_SECONDS=604800` 时报 604800。
|
||
- 既有 `tests/test_vedastro_official_artifact_manifest.py`、`tests/test_vedastro_official_full_snapshot.py`、`tests/test_vedastro_range_scan_replay.py`、`tests/test_three_engine_parity_runner.py`、`tests/test_vedastro_gateway.py`、`tests/test_vedastro_user_entrypoint.py`、`tests/test_vedastro_runtime_ops.py` 全绿。需要 artifact 文件的既有测试改为显式 `monkeypatch.setenv("VEDASTRO_WRITE_AUDIT_ARTIFACTS", "1")`——这算改测试前置,逐条写「原值 / 新值 / 原因」。
|
||
- 分工:Grok 自跑以上;Claude 复验。
|
||
|
||
### T3 · chart 缓存键规范化(BUG-1330)
|
||
|
||
- 新模块里写 `normalize_chart_cache_birth(body)`:`year/month/day` → `int`;`hour/minute/second/lat/lon/tz` 能解析成有限数字的 → `float`(**不四舍五入**,`12`、`12.0`、`"12"` 都成 `12.0`);解析不了的保留原值(不得把不同输入合并);`today` / `transit_date` 取前 10 位。`_build_api_chart_cache_payload` 改用它;`cache_schema_version` 5 → 6(旧缓存自然失效,由 T1 清掉)。
|
||
- `skip_vedastro_main_entry_overview` 维持独立字段(BUG-161);`vedastro_runtime` 指纹不动。
|
||
- **不改** `vedastro_snapshot_cache.snapshot_cache_key`、`consult_full_data_cache.identity_from_body`、`_official_full_snapshot_cache_key`(见决策记录)。
|
||
- 验收标准:
|
||
- 新测试:上面 §2.2 的 7 种变体中,数值等价的变体同键;`tz="Asia/Kolkata"` 之类的非数字时区与 `tz=5.5` 不同键;skip 与非 skip 不同键;`test_api_chart_cache_key_tracks_vedastro_runtime_state` 不改仍绿。
|
||
- **逐字节等价证明**:差分脚本对每组数值等价变体,在 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0` 下各算一次 `_compute_chart_sync`(skip=True 与 skip=False 各一轮),去掉 `runtime_cache` 后 `json.dumps(sort_keys=True)` 必须相同;任何一组不同即说明该字段不能合并,保留原值并写进 PROGRESS。
|
||
- 分工:Grok 自跑;Claude 复验并独立重跑差分脚本。
|
||
|
||
### T4 · 前台快照缓存、负缓存及其它目录的清理(BUG-1332)
|
||
|
||
- 登记进 T1 的表:
|
||
- `vedastro_snapshot_cache/`:`reference_date` 早于「今天(UTC)− `MAX_STALE_DAYS` − 1」即删(与 `lookup_snapshot` 的回看窗口一致,直接引用该常量,不另写数字)。
|
||
- `vedastro_snapshot_negative_cache/`:`reference_date` 早于今天(UTC)即删。
|
||
- `vedastro_gateway_jobs/`:`updated_at` 超 `VEDASTRO_GATEWAY_JOB_KEEP_SECONDS`(默认 86400)且状态为 completed / failed 的 job 及其 `.official_raw_response.json` 一并删;queued / running 不删。
|
||
- `varga_segments/`:mtime 超 `JYOTISH_VARGA_SEGMENTS_KEEP_DAYS`(默认 7)删。
|
||
- 所有删除只认 64 位十六进制文件名或该目录既有的命名格式,其它文件不碰。
|
||
- 不改:这些模块的读写逻辑、键、时效窗口。
|
||
- 验收标准:新测试覆盖每个目录「该删的删、该留的留、命名不符的不碰」;`tests/test_vedastro_snapshot_cache.py`、`tests/test_vedastro_consultation_snapshot.py` 全绿且断言不改。
|
||
- 分工:Grok 自跑;Claude 复验。
|
||
|
||
### T5 · A/B 验证(P1 必做)
|
||
|
||
- 脚本 `scripts/research/api_disk_cache_ab.py`(或 `tools/`,执行方定,写进 PROGRESS),**同进程**分别跑 A(`origin/staging` 原函数,通过 `git show 77057a819:<path>` 导出到临时目录再 import)与 B(新代码):
|
||
- 输入:固定 20 张虚构盘 × 3 种数值写法(`12` / `12.0` / `"12"`,`lat` 字符串 / 数字)× skip/非 skip × 模拟 7 个 `today`(连续日期),每组重复 3 次取中位数。
|
||
- 环境:模拟生产依赖(§1 的 `sitecustomize` 挡 `jhora`,Windows 侧装了 vedastro SDK 则**另跑一组不挡 vedastro 的**,外网可用与否如实记录);`JYOTISH_SCRATCH_ROOT` 或 monkeypatch 指到临时目录,不碰工作树 `scratch/`。
|
||
- 度量:每个目录的文件数与总字节(模拟 7 天后、各自清理跑过之后);chart 缓存命中率(命中次数 / 请求数);单次 `/api/chart` 计算耗时中位数(命中 / 未命中);一次全量清理耗时(目录里预置 10,000 个文件时)。
|
||
- 正确性:B 的 chart 响应与 A 去掉 `runtime_cache` 后逐字节一致(T3 差分脚本复用)。
|
||
- 目标(达不到如实写,不改目标):chart 缓存命中率在三种写法混合输入下 B ≥ A 的 2.5 倍;模拟 7 天后 `api_chart_cache` 文件数 B ≤ 当天活跃键数;`vedastro_adapter` 下 artifact 文件数 B = 0(开关默认关);负缓存目录只剩当天文件;清理 10,000 文件 < 2 s 且不在请求线程。
|
||
- 开关默认值按 A/B 结论定:artifact 开关默认关是决策项,不随 A/B 变;周期清理间隔、负 TTL 可按结论调整并写明理由。
|
||
- 记录写 `docs/testing/api-disk-cache-retention-ab-20261013.md`(表格:改前 / 改后 / 变化;命令可复制)。
|
||
- 分工:Grok 跑并交记录;Claude 在 Linux 上按同一命令复跑一遍,数字差 >20% 时以 Claude 为准并在验收里说明。
|
||
|
||
### T6 · 咨询卡片缓存体积(只记录,本单不改代码)
|
||
|
||
- 在 PROGRESS 写清 §2.5 的数字与两条可选方向(交产品/下一轮决定):(a) 卡片缓存按领域切片存、命中时只读所需领域;(b) `prune_cache` 改为节流(每 N 分钟一次)而不是每次 `get_or_build` 全目录 stat。**不得**做进程内已解析包缓存(BUG-721)。
|
||
- 若 T1 的周期清理已覆盖 `consult_full_data_cache`,可以把 (b) 顺手收进来(把它登记进表、`get_or_build` 去掉每次 `prune_cache()`),但必须证明两天留存语义不变,且 `tests/test_consult_card_full_source.py` 不改仍绿;做不到就不做。
|
||
|
||
### T7 · 记录
|
||
|
||
- `docs/BUG_HISTORY.md` 新增 BUG-1330(chart 缓存不清理 + 键碎片)、BUG-1331(VedAstro artifact 留存出生资料 + 失败快照长缓存 + TTL 诊断失真)、BUG-1332(前台快照/负缓存/网关任务/分盘段缓存无清理);每条写「防复发:新增磁盘缓存必须同时登记留存规则到 `api_scratch_retention`」,并在测试里加一条**合同**:`scripts/` 下新出现的 `scratch/local/<子目录>` 写入点必须在登记表里(grep 源码实现,白名单列出离线脚本)。
|
||
- `docs/tasks/PROGRESS-api-disk-cache-retention-20261013.md`。
|
||
- `CHANGELOG.md`:一句话(用户不可感知,写「服务器不再长期保留出生资料副本」)。
|
||
- `docs/tasks/README.md` 索引加本单。
|
||
- 不改 `frontend/DESIGN.md`(无 UI)。
|
||
|
||
## 7. 让步顺序
|
||
|
||
时间不够时按此顺序**往后砍**:T6(只记录)→ T4 中 `varga_segments` 与 `vedastro_gateway_jobs` → T3 → T4 其余。T1、T2、T5、T7 不可砍;砍掉的写进 PROGRESS「未做」与 `BLOCKED.md`(如因环境)。
|
||
|
||
## 8. 验收命令分工
|
||
|
||
| 谁 | 命令 |
|
||
| --- | --- |
|
||
| Grok 自跑 | `.venv` 下:`python -m pytest tests/test_api_scratch_retention.py tests/test_api_async_job_contract.py tests/test_api_server_growth_contract.py tests/test_vedastro_official_artifact_manifest.py tests/test_vedastro_official_full_snapshot.py tests/test_vedastro_range_scan_replay.py tests/test_three_engine_parity_runner.py tests/test_vedastro_gateway.py tests/test_vedastro_user_entrypoint.py tests/test_vedastro_runtime_ops.py tests/test_vedastro_snapshot_cache.py tests/test_vedastro_consultation_snapshot.py tests/test_consult_card_full_source.py -q`;`python -m pytest tests/test_api_server_security.py -q -k "cache"`;T3 差分脚本;T5 A/B 脚本。前端、Python 快速门、全量 pytest **不跑**,以 Claude 为准 |
|
||
| Claude 验收 | 上述全部 + `.venv/bin/python scripts/run_quality_gate.py --profile quick` + 全量 `pytest` 与 `origin/staging` 基线逐名比对 + 在模拟生产依赖(挡 `jhora`)下复跑 T3 差分与 T5 A/B + `wc -l scripts/jyotish_api_server.py` ≤ 10,970 |
|
||
| 不需要 | 前端任何命令(不改前端);`test:db`(不动数据库) |
|
||
|
||
## 9. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/api-disk-cache-retention-20261013 .worktrees/api-disk-cache-retention-20261013 origin/staging
|
||
cd .worktrees/api-disk-cache-retention-20261013
|
||
git log -1 --format=%h # 应为 77057a819 或其后代
|
||
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号,确认 1330–1332 未被占
|
||
wc -l scripts/jyotish_api_server.py # 记下开工行数
|
||
grep -rn "ARTIFACT_DIR\|artifact_path\|_write_artifact\|prune_expired_async_jobs\|_build_api_chart_cache_payload" scripts tests --include=*.py
|
||
# 键碎片复现:
|
||
PYTHONPATH=.:scripts python - <<'EOF'
|
||
import jyotish_api_server as s
|
||
b=dict(year=1990,month=1,day=1,minute=0,lat=28.6139,lon=77.2090,tz=5.5)
|
||
print(len({s._api_chart_cache_key(s._build_api_chart_cache_payload(dict(b,hour=h))) for h in (12,12.0,'12')})) # 改前 3
|
||
EOF
|
||
```
|
||
|
||
跑完任何 Python 后 `git status --short` 必须为空(`scratch/` 已在 `.gitignore`,但 A/B 必须写临时目录)。
|
||
|
||
## 10. 环境缺口(预告)
|
||
|
||
- 生产 / staging 的 `VEDASTRO_CACHE_TTL_SECONDS`、`VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 实际值未知:部署后由产品负责人在服务器内网查 `/api/vedastro_gateway/status` 的两个 TTL 字段(T2 修正后含回落),只回报数字,不贴其它内容。
|
||
- 生产卷上已有的存量文件数量与体积未知:部署后可让运维在容器内跑 `du -sh /app/scratch/local/*`(只回报各目录体积与文件数)。新清理上线后首次启动即会清掉按规则可删的部分。
|
||
- 真实 VedAstro 外网下的快照文件体积、成功率:本机无 SDK、无外网,A/B 只能在 Grok 侧(若有 SDK 与外网)或 staging 部署后复核。
|