Files
lionwebsite-backend/docs/SUBSCRIPTION_CACHE_REWRITE.md
T

4.4 KiB
Raw Blame History

订阅缓存分代化 — 重写模块说明

问题

一个订阅账号对应两份内容:v2ray.txt(Base64 节点)与 clash.yaml(Clash 配置)。 旧实现先 atomicWrite 写 v2ray,再 atomicWrite 写 clash,两次替换彼此独立:

  1. 两份文件可以不同版本。第二次写入失败(磁盘满、进程被杀)时,用户会拿到 「新 v2ray + 旧 clash」的混合版本,且没有任何状态能表明它已经撕裂。
  2. 读取方无法保证配对。分发路径与备机快照各自 Files.readAllBytes 两个路径, 在这两次读之间完成一次刷新,就会读到跨版本组合。
  3. 过期刷新可能复活。在途刷新返回时只比较上游 key 与过滤开关;一次 invalidateCache 之后,迟到的响应仍会把内容写回被清空的路径。
  4. 失败留半成品。第一次写成功、第二次失败时,磁盘上就是只有一份文件的账号。

交付物与职责

文件 职责
Util/SubscriptionCacheStore.java 分代存储:两份文件同一分代写入 → 校验 → 指针原子发布;读取始终来自指针指向的同一分代
Service/SubscriptionRefreshService.java 抓取与解析在协调器锁外进行,发布短持锁;invalidateCache 递增版本使在途刷新失效
Service/SubscriptionStandbySnapshotService.java 快照构建在读锁内通过同一 store 解析成对内容,保证同版本
Util/ClashSubscriptionParser.java 按 YAML 语义过滤高倍节点并清理 proxy-groups 引用(由主模型提供)

目录布局

<cacheRoot>/<accountId>/current                      指针(Properties 文本,原子替换)
<cacheRoot>/<accountId>/generations/<generation>/v2ray.txt
<cacheRoot>/<accountId>/generations/<generation>/clash.yaml
<cacheRoot>/<accountId>/v2ray.txt + clash.yaml        旧结构,仅在两份都在时按 legacy 读

current 记录分代名与两份文件的 SHA-256。分代名是定宽 时间戳-序号, 因此字典序即时间序;默认只保留最近两个分代(当前与上一个)。

不变量

  • 发布最后一步才切指针:先写两份文件,读回校验长度与内容,再原子替换 current。 抓取失败、解析失败、任一份写入失败、指针写入失败,都会回滚新分代目录并保留上一个完整版本。
  • 读取配对:read / resolve / hasComplete 走同一份 load 契约,每个调用只读一次 指针快照,再按该快照校验双文件哈希。只有两份齐全且匹配才算一个有效版本; 指针缺失才回退 legacy(两份齐全时),指针存在但读不出完整版本(半写、篡改、缺字段) 时按无可用缓存处理,不回退到更旧的半份内容。
  • 分发不会发出半份缓存:cachedPath 在无有效版本时返回保证不存在的哨兵路径 (.unavailable/),使 SubService 判定为 503,而不会被单份 legacy 文件骗过。 SubService.updateSub 在读锁内完成 resolve 与 FileDownload.export,与 publish/invalidate 互斥。
  • 失效不可复活:invalidateCache 在写锁内删除指针/分代/旧结构文件,并移除该账号的 刷新版本标记;在途响应返回时因版本标记对不上而被丢弃,不会重新发布。
  • 兼容旧结构:只有当账号根下 v2ray.txt 与 clash.yaml 都在时才读为 legacy 版本; 新分代发布成功后旧结构文件被移除,避免两份来源并存。

测试

测试 锁定行为
Util/SubscriptionCacheStoreTest 发布后两份同分代;第二份写入失败 / 指针发布失败均保留旧版本;legacy 双文件才有效;半份 legacy、缺一份分代、内容被篡改、坏指针、指针指向缺失分代均判定不可用;每次查询只读一次指针;哨兵路径保证不存在
Service/SubscriptionCacheGenerationTest 在途旧刷新在失效与新版发布后被丢弃;被丢弃的刷新不留任何缓存
Service/SubscriptionFilteringTest 过滤规则;失败保留上次完整版本;cachedPath / hasCompleteCache 新契约;legacy 路径定位
Service/SubscriptionStandbySnapshotServiceTest 快照从单一分代读成对内容;不完整 legacy 账号不入快照
Util/ClashSubscriptionParserTest YAML 语义过滤(内联节点与引用)与畸形正文拒绝