4.4 KiB
4.4 KiB
订阅缓存分代化 — 重写模块说明
问题
一个订阅账号对应两份内容:v2ray.txt(Base64 节点)与 clash.yaml(Clash 配置)。
旧实现先 atomicWrite 写 v2ray,再 atomicWrite 写 clash,两次替换彼此独立:
- 两份文件可以不同版本。第二次写入失败(磁盘满、进程被杀)时,用户会拿到 「新 v2ray + 旧 clash」的混合版本,且没有任何状态能表明它已经撕裂。
- 读取方无法保证配对。分发路径与备机快照各自
Files.readAllBytes两个路径, 在这两次读之间完成一次刷新,就会读到跨版本组合。 - 过期刷新可能复活。在途刷新返回时只比较上游 key 与过滤开关;一次
invalidateCache之后,迟到的响应仍会把内容写回被清空的路径。 - 失败留半成品。第一次写成功、第二次失败时,磁盘上就是只有一份文件的账号。
交付物与职责
| 文件 | 职责 |
|---|---|
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 语义过滤(内联节点与引用)与畸形正文拒绝 |