服务器缓存按期限清理,审计快照默认不再落地

排盘缓存、官方快照、负缓存、网关任务和分盘段过期后会删掉。同一张盘的数字写法合成一把缓存键。失败快照只用短期限。自测留存、异步任务、增长合同、安全缓存和隐私标记;一万个文件的清理慢于 2 秒,命中率提升不到 2.5 倍,都按实测写入记录。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 04:53:30 +08:00
co-authored by Grok (agent loop)
parent 66cd60081f
commit 81e2e1cfed
14 changed files with 1677 additions and 65 deletions
+45
View File
@@ -17795,3 +17795,48 @@
- 复发自:无。
- 修复版本:未修复。
## BUG-1330 | 排盘缓存只进不出,同一张盘还会存成多份
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
- 来源:issue #60。任务书 `docs/tasks/TASK-api-disk-cache-retention-20261013.md` T1、T3。
- 现象:排盘结果写在磁盘缓存里。过期只在读取时判断,文件一直留着。启动时的清理扫的是异步任务目录,不是这份缓存。同一张盘因为小时、经纬度写成数字或字符串,会各存一份。
- 触发条件:反复排盘,或同一次出生时间用不同写法提交。服务长时间不重启时,异步任务记录也会一直留到被轮询。
- 根因:写缓存的人只做了读取时的过期判断。写清理的人以为缓存和异步任务在同一个目录、同一种记录。缓存键直接用了请求里的原值,计算前却会先把这些值解析成数字。
- 修复:新的留存模块按缓存写入时间和排盘缓存期限删除过期文件,坏文件超过一天也删。启动时清理一次,之后按周期在后台再清。新写入的缓存文件改为仅本人可读,并先写临时文件再换上。缓存键把能解析的数字收成同一种写法,缓存格式版本从 5 改为 6,旧文件自然失效后被清掉。快速排盘和完整排盘仍然是两把不同的键。
- 验证:`tests/test_api_scratch_retention.py`、`tests/test_api_async_job_contract.py`、`tests/test_api_server_security.py -k cache`、`tests/test_api_server_growth_contract.py`。数值写法的逐字节比较见 `scripts/research/api_chart_cache_byte_diff.py`。
- 防复发:新增磁盘缓存必须同时在 `scripts/api_scratch_retention.py` 登记留存规则。`tests/test_api_scratch_retention.py` 会检查 `scripts/` 里新的 `scratch/local/` 目录有没有登记。
- 相关记录:BUG-161、BUG-1331、BUG-1332。
- 复发自:无。
- 修复版本:分支 `codex/api-disk-cache-retention-20261013`,尚未合入 staging。
## BUG-1331 | VedAstro 审计文件留着出生资料,失败快照被缓存太久
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
- 来源:issue #60。任务书 T2。
- 现象:每次官方快照归一都会在磁盘留一份带缩进的审计文件,里面有出生时间。失败、不完整、被挡住的快照和成功结果用同一个长缓存期限。状态接口报出的期限没有算上回落值,可能和实际不一致。
- 触发条件:在线排盘或报告走到官方快照。样例环境把成功缓存写成 7 天时,一次失败也会被原样复用 7 天。
- 根因:审计落盘最早只给离线比对用,后来同一条归一进了在线请求,开关没有跟着关掉。语义缓存把「拿到一个结果」当成「可以长期复用」。状态接口自己读了一个变量,没有走实际生效的那个函数。
- 修复:审计落盘默认关闭,需要比对时再显式打开。打开时改为紧凑文件、仅本人可读、先写临时文件再换上。失败、不完整、被挡住的快照用短期限,默认 300 秒,并且不会长过成功期限。状态接口改为报出含回落的成功期限,并单独报出失败期限。过期的语义缓存、业务响应缓存和审计文件由留存模块删除。
- 验证:`tests/test_api_scratch_retention.py` 覆盖默认关闭、打开后的文件、失败 301 秒后不再命中、状态接口回落。既有 VedAstro 适配器测试仍通过。需要审计文件的那一条测试改为显式打开开关。
- 防复发:新增磁盘缓存必须同时在 `scripts/api_scratch_retention.py` 登记留存规则。
- 相关记录:BUG-734、BUG-301、BUG-1330。
- 复发自:无。
- 修复版本:分支 `codex/api-disk-cache-retention-20261013`,尚未合入 staging。
## BUG-1332 | 前台快照、负缓存、网关任务和分盘段缓存没有清理
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
- 来源:issue #60。任务书 T4。
- 现象:前台快照缓存超过回看窗口后不会再被读,但文件不删。负缓存只认当天,昨天的文件一直在。网关任务和分盘段缓存也只增不减。这些目录在持久卷上,部署不会清空。
- 触发条件:使用前台官方快照、咨询负缓存、网关队列或校正分盘段。
- 根因:当初只规定了什么时候还能读,没有规定什么时候可以删。
- 修复:前台快照早于「今天减去回看天数再减一天」就删。负缓存早于今天(UTC)就删。已完成或失败、并且超过保留时间的网关任务连同原始响应一起删;排队中和进行中的不删。分盘段按修改时间保留,默认 7 天。只删符合原有文件名的文件,其它文件不碰。读写和缓存键都没有改。
- 验证:`tests/test_api_scratch_retention.py` 覆盖这四个目录的留、删和不碰。`tests/test_vedastro_snapshot_cache.py` 与 `tests/test_vedastro_consultation_snapshot.py` 断言未改。
- 防复发:新增磁盘缓存必须同时在 `scripts/api_scratch_retention.py` 登记留存规则。
- 相关记录:BUG-727、BUG-1231、BUG-1330。
- 复发自:无。
- 修复版本:分支 `codex/api-disk-cache-retention-20261013`,尚未合入 staging。
@@ -0,0 +1,51 @@
# PROGRESS · API 磁盘缓存留存(2026-10-14)
- 分支:`codex/api-disk-cache-retention-20261013`
- 任务书基线写的是 `77057a819`。本分支从当时的 `origin/staging` `66cd60081f37a57680274633fcb3fb942d01bb2f` 拉出。A/B 的父提交用的是这个 SHA,不是任务书里的旧基线。
- 接口文件行数:10946(验收口径 ≤ 10970)。类方法数和伪造点没有增加。
## 做了什么
- T1:新增 `scripts/api_scratch_retention.py`。启动时清理全部已登记目录,并按 `JYOTISH_SCRATCH_PRUNE_INTERVAL_SECONDS`(默认 1800,0 表示不启后台线程)周期清理。排盘缓存新文件改为仅本人可读,先写临时文件再换上。`prune_expired_async_jobs` 只留同名薄包装。
- T2:`VEDASTRO_WRITE_AUDIT_ARTIFACTS` 默认关。关掉时不写审计文件,`artifact_path` 为 `None`。打开时写紧凑 JSON,仅本人可读,先写临时文件再换上。成功快照仍用原 TTL;失败、不完整、被挡住的用短 TTL,默认 300 秒,并且不超过成功 TTL。旧缓存没有 `negative` 标记时仍按成功的长期限读。状态接口改报含回落的成功期限,并多报失败期限。`.env.cn.example` 只加了注释,两个 604800 没改。
- 成功的判据:`available is True` 且 `status == "ok"`。`partial` 即使 `available` 为真也算失败缓存。
- T3:年月日能整除才收成整数,不截断 1990.5。时分秒和经纬度、时区能解析成有限数字就收成浮点,不四舍五入。解析不了的保持原值。日期取前 10 位,空值仍是空值。缓存格式版本 5 → 6。快速排盘和完整排盘仍是两把键。
- T4:前台快照、负缓存、网关任务、分盘段都进了登记表。只删符合原文件名的过期文件。网关排队中和进行中的不删。读写逻辑和键没改。
- T5:脚本在 `scripts/research/api_disk_cache_ab.py`,逐字节比较在 `scripts/research/api_chart_cache_byte_diff.py`。记录在 `docs/testing/api-disk-cache-retention-ab-20261013.md`。
- T6:没有改咨询卡片缓存。数字用任务书 §2.5,没有重测:未命中构建 1.9 秒;文件 5097751 字节;命中时解析中位 68.5 毫秒;整次命中 82.5 毫秒;两天大约每个活跃用户 10 MB。可选方向仍是两条:按领域切片只读需要的部分;清理改为每隔几分钟一次,而不是每次读取都扫全目录。没有做进程内已解析包缓存。
- T7:BUG-1330、BUG-1331、BUG-1332。变更说明一句。任务索引改为待验收。没有改界面说明。
## 命令和结果
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 新留存、异步任务、增长合同 | `pytest tests/test_api_scratch_retention.py tests/test_api_async_job_contract.py tests/test_api_server_growth_contract.py -q` | 26 通过 |
| 安全缓存 | `pytest tests/test_api_server_security.py -q -k cache` | 6 通过 |
| 任务书列出的 VedAstro / 咨询套件 | 见任务书 §8 那一条 | 4 失败,见下。其余通过 |
| 需要审计文件的执行器测试 | `pytest tests/test_vedastro_service_adapter_executor.py::test_vedastro_range_scan_records_hashes_and_artifact_path -q` | 1 通过 |
| 逐字节比较与 A/B | `python scripts/research/api_disk_cache_ab.py` | 退出码 0。命中率 B/A = 1.333。七天后 B 剩 40 个文件,等于当天键数。审计文件 0。负缓存剩 1。一万个文件的清理 20.174 秒 |
| 接口行数 | 按换行计数 | 10946 |
既有断言改动只有一处,在 `tests/test_vedastro_service_adapter_executor.py`:原值是子进程沿用进程环境;新值是显式打开审计落盘;原因是默认关闭后这条测试仍要检查文件存在。这是测试前置,不是放宽断言。
## 未达到的目标(目标不改)
- 混合写法再重复 3 次时,命中率只是 A 的 1.333 倍,不是 2.5 倍。重复本身已经让 A 命中三分之二。
- 一万个过期文件的清理用了 20.174 秒,不是 2 秒内。清理不在请求线程。默认间隔和失败短期限没有因此改掉。
## 本机套件里原先就会红的三条
- `tests/test_vedastro_gateway.py` 要求原始响应文件权限是 0600。Windows 报出来的是可写,不是 0600。这条断言没改。新测试在 Linux 上仍检查 0600,Windows 上只检查文件写出来了。
- `tests/test_vedastro_runtime_ops.py` 要求已安装的 VedAstro 是 1.23.25,本机是 1.23.30。与这次缓存改动无关。
- `tests/test_three_engine_parity_runner.py` 的公开盘回放把 PyJHora 记成 raw_imported,不是 structured_captured,同时在下载星历。运行器没改。
## 环境缺口
- 生产和测试环境里成功缓存的实际秒数,这份仓库读不到。部署后看状态接口的两个期限数字即可。
- 生产卷上已经堆了多少文件,这里不知道。
- 主测量挡住了 VedAstro 导入。本机有 SDK,官方端口能连通。不挡 SDK 的探针仍关闭外网取数,一次完整排盘 4.167 秒,没有报错,没有拉真实快照。
## 未做
- 没有改部署文件、生产环境变量、数据库,也没有手工清生产卷。
- 没有把咨询卡片缓存并进周期清理。
+1 -1
View File
@@ -470,7 +470,7 @@
| `TASK-rect-scoring-perf-20261013.md` | — | 校正打分提速:引导窗口平方扫描、候选不变量重复算、响应带无读取点字段、候选数上限 241(BUG-1323~1325、1345) | 待领取 | `codex/rect-scoring-perf-20261013` |
| `TASK-node-runtime-resources-20261013.md` | — | 报告 worker abort 监听泄漏、本地 PG 适配层往返与元数据缓存、无上限内存表、用户端会话读缓存(BUG-1326~1328、1346,PG 与缓存部分 A/B) | 待领取 | `codex/node-runtime-resources-20261013` |
| `TASK-home-sidebar-rerender-20261013.md` | — | 流式回答时侧栏每帧整栏重渲染(BUG-1329,A/B) | 待领取 | `codex/home-sidebar-rerender-20261013` |
| `TASK-api-disk-cache-retention-20261013.md` | — | API 磁盘缓存只进不出、VedAstro 审计文件留存出生资料、失败快照久缓存、缓存键规范化(BUG-1330~1332,A/B) | 待领取 | `codex/api-disk-cache-retention-20261013` |
| `TASK-api-disk-cache-retention-20261013.md` | `PROGRESS-api-disk-cache-retention-20261013.md` | API 磁盘缓存只进不出、VedAstro 审计文件留存出生资料、失败快照久缓存、缓存键规范化(BUG-1330~1332,A/B) | 待验收 | `codex/api-disk-cache-retention-20261013` |
| `TASK-report-reference-perf-20261013.md` | — | 长报告出稿提速:术语替换命中预检、哈希单次遍历、同盘复用(BUG-1335~1336,A/B) | 待领取 | `codex/report-reference-perf-20261013` |
| `TASK-consult-prep-parallel-20261013.md` | — | 普通对话发问前准备步骤并行、模型版本短缓存、多领域并发开关(BUG-1337~1339,A/B) | 待领取 | `codex/consult-prep-parallel-20261013` |
| `TASK-frontend-dead-code-20261013.md` | — | 前端死代码 17 文件与无调用方接口删除,含未登录重计算接口(BUG-1342) | 待领取 | `codex/frontend-dead-code-20261013` |
@@ -0,0 +1,66 @@
# 磁盘缓存留存 A/B(2026-10-14)
对照的是本分支的父提交 `66cd60081f37a57680274633fcb3fb942d01bb2f`(建分支时的 `origin/staging`)。任务书里写的 `77057a819` 已经不是当前测试线头。A 用 `git show` 把父提交的接口文件载入同一进程,并把它的仓库路径指回本工作树,否则临时副本找不到引擎。B 是本分支。
演练用虚构盘,不写真实出生资料。文件都在系统临时目录,不写工作树里的 `scratch/`。
主测量挡住了 `jhora` 和 `vedastro` 的导入。本机装了 VedAstro SDK,并且能连上官方端口;另有一组不挡 SDK、但仍关闭外网取数,见文末。
## 命令
```powershell
.venv\Scripts\python.exe scripts\research\api_chart_cache_byte_diff.py
.venv\Scripts\python.exe scripts\research\api_disk_cache_ab.py
.venv\Scripts\python.exe scripts\research\api_disk_cache_ab.py --with-sdk --sdk-probe
```
## 命中率
20 张虚构盘 × 3 种写法 × 快速/完整 × 7 天 × 每种重复 3 次 = 2520 次取键。取键用的是真实缓存键函数,没有把 2520 次都做成完整排盘。
| | 改前 A | 改后 B | 变化 |
| --- | --- | --- | --- |
| 不同缓存键 | 840 | 280 | 少 560 |
| 命中次数 | 1680 | 2240 | 多 560 |
| 命中率 | 0.6667 | 0.8889 | B / A = 1.333 |
目标是混合写法下 B 至少为 A 的 2.5 倍。按任务书规定的「重复 3 次」算,没有达到:A 自己靠重复就已经有三分之二命中,写法合并只能再抬到约 89%。若不算这 3 次重复、只看三种写法各问一次,A 的命中率是 0,B 是 0.6667。目标数字不改。
## 七天后的文件
留存演练写的是短记录,用来数文件,不是完整盘面。期限 900 秒,只有当天的键还没过期。
| | 改前 A | 改后 B |
| --- | --- | --- |
| 清理前文件数 | 840 | 280 |
| 清理前字节 | 109200 | 36400 |
| 清理后文件数 | 840(旧代码没有这份清理) | 40 |
| 当天仍有效的键 | 120 | 40 |
B 清理后的文件数等于当天有效键数,达到「不超过当天活跃键」的目标。
审计文件:B 在默认关闭时写出 0 个。达到目标。
负缓存:种入当天和前一天各一份,清理后剩 1 份(当天)。达到目标。
## 清理耗时
| 场景 | 结果 |
| --- | --- |
| 预置 10000 个已过期排盘缓存后,整次清理 | 20.174 秒,排盘目录 20170.902 毫秒,删掉 10000,剩下 0 |
| 上面七天演练里的排盘目录 | 370.161 毫秒 |
目标是 10000 个文件在 2 秒内、且不在请求线程。本次在后台函数里测,不在请求线程;耗时没有进 2 秒。这是 Windows 上逐个读文件再删除的实测,目标不改。默认清理间隔仍是 1800 秒,失败快照短期限仍是 300 秒,不因为这次没进 2 秒就改默认值。
## 排盘耗时(一次虚构盘,快速排盘,外网关闭)
| | 中位数 |
| --- | --- |
| 未命中 3 次 | 23.036 毫秒 |
| 命中 3 次 | 5.249 毫秒 |
## 响应是否还是同一张盘
数值等价的写法(小时 12 / 12.0 / "12",纬度数字和字符串,时区 5.5 和 "5.5",秒缺省和 0)在快速排盘下去掉 `runtime_cache` 后逐字节相同。完整排盘只差官方快照缓存上的写入时刻和到期时刻,盘面没有别的差异。父提交和本分支对同一张快速排盘、去掉 `runtime_cache` 后逐字节相同。因此这些字段可以合并进同一把缓存键。
不挡 SDK 的一组:本机有 VedAstro SDK,官方端口能连通。探针仍关闭外网取数。一次完整排盘 4.167 秒,没有报错。没有拉取真实快照。