跳转至

SOP — cache_probe 快照过期即判无缓存 + systemd 服务修复

变更日期:2026-10-11 实施人:rain(Agent 执行) 类型:C 类(改代理代码 + 恢复 systemd 托管) 归档目录:token_proxy/cacheprobe-expired-20261011/

1. 背景

两条触发链:

  1. 超长空闲未拦截:本会话空闲 31.4 小时后,cache_probe 未按设计拦截,代理付了整段前缀重建(本轮实测未命中 91%)。核查发现:探针的快照有 24 小时 TTL(SNAP_TTL_SECONDS = 24*3600),超期条目在启动加载时被丢弃;空闲 31 小时超过 TTL,探测前提「该会话存在一份上次快照」不成立,_due_snapshot 返回 None,探针静默放行。磁盘证据:快照文件仅剩 4 条、全在 7.6 小时内,usage.jsonl 最近一条探测停在 10-10 00:20。

  2. systemd 服务 failed:token-proxy.service 自 16:59:32 起为 failed。日志显示 16:59 起反复 Main process exited, code=exited, status=1/FAILURE,重启 6 次后触发 Start request repeated too quickly。proxy.stdout.log 尾部的 traceback 给出根因:OSError: [Errno 98] Address already in use(端口 8788 被占)。此后 17:14 有人手动启动代理占住 8788,systemd 无法接管。

2. 目标

  1. token-proxy.service 恢复 systemd 托管,active(running),8788 由 systemd 的 MainPID 监听;
  2. 会话快照年龄超过 TTL 时,cache_probe 直接判定「无缓存」并拦截,不再因快照被丢弃而静默放行;
  3. 空闲 1h~24h 的探测路径、空闲 <1h 的放行路径行为不变;
  4. 改码经离线验证通过、两遍同结果。

3. 前置条件

  • 代理进程与 config.yaml、logs/ 就绪;
  • venv/ 可用(venv/bin/python);
  • 无在跑的关键业务依赖(本变更重启代理,中断秒级)。

4. 影响范围

项 内容
主机 本机 dell-R620(代理与客户端同机)
文件 proxy.py(4 处改动);新增交付目录 cacheprobe-expired-20261011/
是否停服 是,代理重启一次(秒级中断)
窗口期 随时(可随时重启,不打断会话输出流)
回滚点 proxy.py 改前快照(本主题 backup/)

5. 分步操作

实现体:proxy.py(CacheProbe 类)。以下给出等价底层操作供阅读与复核。

步骤 1:修 systemd 服务

根因是端口 8788 被手动进程占用,systemd 起的进程 bind 失败。修法:停占端口的手动进程,重置失败计数,再起服务。

# 停掉占用 8788 的手动进程(PID 以 ss 实测为准)
ss -ltnp | grep :8788                      # 确认占用者 PID
kill -TERM <占用PID>                        # 高危:停止进程,触发优雅退出(落盘快照)
sleep 3
ss -ltnp | grep :8788 || echo PORT_FREE     # 预期 PORT_FREE
systemctl --user reset-failed token-proxy   # 清掉「重启过快」限流计数
systemctl --user start token-proxy

预期:systemctl --user is-active token-proxy 输出 active,ss -ltnp | grep :8788 的 PID 等于 systemctl --user show token-proxy -p MainPID。 失败判断:若端口未释放则 start 仍报 Address already in use,回到第 7 章。

步骤 2:改 proxy.py:过期快照保留标记

load_state() 原逻辑把超 TTL 条目 continue 丢弃,导致「曾见过的会话」与「新会话」不可区分。改为保留一个轻量过期标记(只存 ts)。

# 见 proxy.py::CacheProbe.load_state():超 TTL 条目改存 self._expired_snap(ts)
# 新增 proxy.py::CacheProbe._expired_snap():返回 {ts, expired: True, messages: None, ...}
# 改 proxy.py::CacheProbe._snap_for_state():对 expired 条目只落 {ts, expired: True}

步骤 3:改 proxy.py:过期即判失效

check() 在 _due_snapshot() 拿到快照后,若 snap["expired"] 或 idle > SNAP_TTL_SECONDS,直接给出拦截判定,不再发探测请求(省 0.03 元/次探测费)。

# proxy.py::CacheProbe.check():新增分支
#   if snap.get("expired") or snap["idle"] > SNAP_TTL_SECONDS:
#       verdict = {"cached":0,"total":0,"ratio":0.0,"idle":snap["idle"],
#                  "reason":"snapshot_expired"}

配套在 proxy.py::ProxyHandler._reject_cache_expired() 增加 reason == "snapshot_expired" 分支,返回 500 并给出「快照已过期(空闲 X 分钟,超过 24 小时)」文案。

坑:_snap_for_state() 会对快照调用 _truncate_messages(),而过期标记的 messages 为 None,若不先拦就会迭代 None 报错。故必须先判 expired 再返回。

步骤 4:离线验证

venv/bin/python -m py_compile proxy.py
venv/bin/python cacheprobe-expired-20261011/offline_verify.py

预期:COMPILE_OK + RESULT: ALL_OK(A 过期拦 / B 新鲜放行 / C 探测失效拦 / D 探测命中放行 / E 恢复过期标记)。

步骤 5:重启生效

步骤 1 的 systemctl --user start 已用改后代码启动;若先改码后重启,顺序为改码 → 离线验证 → 步骤 1。

6. 验证

# 命令 实测结果
1 systemctl --user show token-proxy -p MainPID -p ActiveState -p SubState -p NRestarts MainPID=3391276、active、running、NRestarts=0
2 ss -ltnp \| grep :8788 PID 3391276 监听 8788(= MainPID)
3 venv/bin/python cacheprobe-expired-20261011/offline_verify.py(两遍) 均 ALL_OK(A/B/C/D/E 五路径)
4 logs/proxy.log 尾部 2026-10-11 20:53:29 代理启动: 0.0.0.0:8788
5 真实拦截路径(快照龄 >24h) 待观察:需等某会话空闲 >24h,或手工把快照 ts 改旧后发请求(会拦断当前对话,未做)

7. 回滚

  1. 还原代码: bash cp -a cacheprobe-expired-20261011/backup/proxy.py.bak.20261011-205209 proxy.py
  2. 重启代理: bash systemctl --user restart token-proxy
  3. 回滚后验证:systemctl --user is-active token-proxy 为 active;venv/bin/python cacheprobe-expired-20261011/offline_verify.py 中 A/E 两项应失败(旧代码丢弃过期快照),即回到变更前语义。

备份位置与清单见 backup/README.md。systemd 侧本次无 unit 文件改动,无需回滚。

8. 风险说明

风险 缓解
误拦 >24h 空闲的会话(本设计意图,但可能打断) 拦截返回 500 并提示「压缩上下文或新开会话」;冷却 120s、压缩后(chars ≤ 90%)立即放行
改 proxy.py 致代理起不来 token_proxy 红线:改前备份 + 离线验证;本次离线 5 路径全过、语法检查通过
停手动进程期间代理中断 秒级;规则明确重启代理不是红线、不打断输出流
_snap_for_state 迭代 None 已先判 expired 再返回(见步骤 3 坑)
systemd 失败根因(端口冲突)复发 本次已清手动进程;若再现,按步骤 1 查 ss 占用者

附录:交付物清单

文件 说明
SOP.md 本文件
offline_verify.py 离线验证脚本(5 路径)
backup/proxy.py.bak.20261011-205209 proxy.py 改前快照
backup/README.md 备份台账

来源:/home/rain/Documents/services/token_proxy/cacheprobe-expired-20261011/SOP.md | 最后更新:2026-10-11 20:54(由 build/sync.py 同步,请勿在站点侧修改)