第 5 章:四层配置与 Draft 事务模型
Clash Verge Rev 的配置难点不在 YAML 语法,而在“候选值、正式值、磁盘值和核心实况”可以短暂不同。本章拆解四类配置、COW Draft、异步乐观锁和跨层事务,建立后续所有配置章节的基础。
一、先区分四种配置对象
Config 是一个进程级异步单例,内部包含四个 Draft<T>:
1.1 IClashTemp:应用控制的 Mihomo 设置
它用 serde_yaml_ng::Mapping 保存,因为 Mihomo 配置字段多且持续演进。应用重点维护:
- mixed/socks/http/redir/tproxy 端口;
- mode、allow-lan、ipv6、log-level;
- external controller、secret 与 CORS;
- TUN 配置。
IClashTemp::template() 生成跨平台默认值,guard() 修复危险或缺失字段。例如 controller 会被限制到应用认可的地址与本地 IPC。
1.2 IVerge:桌面产品设置
这是一个大型强类型 struct,包含:
- 主题、语言、布局与通知;
- 系统代理、PAC、proxy guard;
- TUN、端口、DNS 页面开关;
- tray、hotkey、autostart、lightweight;
- 更新、备份、Web UI 与测试项。
它描述的是应用意图,不等于 Mihomo 最终配置。
1.3 IProfiles:配置素材索引
profiles.yaml 不把所有订阅内容内嵌进去,而是保存:
- 当前 Profile uid;
PrfItem列表;- 每项的文件名、类型、URL、更新时间和增强关联;
- 节点选择记录。
具体 YAML/JS 片段位于 profiles 目录的独立文件,便于更新、编辑和清理。
1.4 IRuntime:编译产物
Runtime 包含:
- 最终
Mapping; - 原 Profile/增强过程中出现过的 key 集合;
- Script 执行日志。
它可以重新生成,不是用户直接维护的源配置。
二、一个值为什么需要 committed 和 draft
普通配置写法常是:
修改内存 → 写文件 → 通知其他模块但这里修改之后可能还要生成 runtime、让 Mihomo 校验、重启核心、设置系统代理。任何一步失败都不能让全局读者看到“已经生效”的值。
Draft<T> 因此保存:
(committed_snapshot, optional_draft_snapshot)对应三种操作:
| 操作 | 含义 |
|---|---|
edit_draft | 从 committed 延迟复制并修改候选值 |
apply | 将 draft 提升为 committed |
discard | 放弃候选值,保留 committed |
三、data_arc 与 latest_arc 是两个不同问题
3.1 data_arc():已经承诺了什么
返回 committed。适用于:
- 已成功提交后的持久化;
- 对外报告正式设置;
- 失败恢复时重新生成旧配置。
3.2 latest_arc():当前正在尝试什么
优先返回 draft,没有 draft 才返回 committed。适用于:
- 用候选设置生成 runtime;
- 计算 patch 引发的副作用;
- 重启核心前构造新配置。
若所有调用点都只用 latest_arc(),失败后仍可能把未提交值写盘;若都只用 data_arc(),校验永远看不到候选修改。
四、为什么值是 Arc<Box<T>>
SharedDraft<T> = Arc<Box<T>>,读快照时只 clone Arc。修改时通过 Arc::make_mut 实现 copy-on-write:
没有其他读者 → 直接取得可变引用
仍有旧快照读者 → clone T,旧读者继续看原值这解决了两个问题:
- 大型
IVerge/IProfiles不必每次读取都 clone; - 异步任务可以持有稳定快照,不受后续 patch 影响。
额外的 Box 让 Arc 内保存固定大小指针,减小外层结构尺寸;真正收益主要仍来自 Arc 快照和 COW。
五、短修改与长异步修改不是同一条路径
5.1 edit_draft:同步闭包
锁只覆盖从 Arc 取出 draft、make_mut 和闭包修改。闭包不能 await,避免持锁跨异步边界。
5.2 with_data_modify:异步所有权更新
某些 Profile 更新需要下载、读写文件。它使用不同策略:
- 原子标志 + Notify 串行化长修改;
- clone committed 到局部所有权;
- 异步闭包在无锁状态执行;
- 返回后比较 committed Arc 指针;
- 若期间被其他提交替换,报 optimistic lock failed;
- 否则原子替换 committed。
这是一种轻量乐观并发控制。它避免长时间占用 Mutex,又不会让晚到的异步结果覆盖新值。
六、单层 Draft 还不够
修改端口会同时影响:
IClashTemp:Mihomo listener;IVerge:界面选择和 enabled 状态;IRuntime:最终运行时 YAML。
若三层分别 apply,第二层失败时第一层已经提交,系统就会自相矛盾。
DraftTransaction 接收不同类型的 DraftLayer:
let tx = DraftTransaction::begin(vec![&clash, &verge, &runtime])?;
// stage changes across layers
tx.commit();6.1 claim:拒绝交叠事务
每层有 claimed: AtomicBool。begin 依次 claim;任意一层已被占用,就释放此前 claim 并返回 DraftBusy。
为什么不排队或合并?每层只有一个 draft slot。两个 writer 交叠时,后者会在同一 candidate 上修改,任何一方 rollback 都可能丢掉另一方的改动。没有版本信息就无法“只回滚我自己的部分”,因此拒绝是唯一诚实的策略。
6.2 drop 默认回滚
Transaction 是 must-use,未 commit 就 drop 时会:
- discard 所有 layer 的 draft;
- release 所有 claim。
这让 ?、early return 和 panic 默认走安全方向。忘记 commit 最多导致操作失败重试;忘记 rollback 会留下虚假状态。
七、内存事务无法自动覆盖文件
DraftTransaction 只管理内存。若已经写了 clash-verge.yaml、verge.yaml 和 runtime 文件,内存 rollback 不会把文件恢复。
项目用 config/snapshot.rs 捕获:
- 文件原内容;
- 文件原本是否存在。
失败时先 transaction.rollback(),再 restore_files()。顺序不能反:文件恢复或重新生成会读取 committed,必须先保证内存已经回到旧值。
八、配置状态的五个时间点
一次可靠 patch 可以分成:
T0 committed = old, draft = none
T1 committed = old, draft = candidate
T2 runtime(candidate) 已生成和校验
T3 外部副作用已成功,commit candidate
T4 committed 文件已写,事件已发失败策略:
- T1/T2 失败:discard draft;
- 文件写到一半:rollback draft + restore snapshots;
- 核心替换失败:恢复旧文件并重新启动旧核心;
- 事件发射失败:正式状态仍成立,记录错误并允许前端下次读取恢复。
九、为什么不是数据库事务
数据库能解决结构化数据原子提交,但不能自动事务化:
- Mihomo 子进程;
- 系统代理;
- TUN 设备;
- 托盘菜单;
- 文件型用户配置;
- 特权 Service。
这里更接近 Saga:内存使用事务,文件使用快照,外部状态使用补偿动作和 generation guard。理解这一点后,项目中看似“重复”的恢复代码就有了共同模型。
十、可迁移的设计原则
- 把候选状态做成一等概念,不要用注释约定“这个对象暂时还没生效”。
- 读 API 要表达语义:
committed()与latest()比一个含糊的get()更安全。 - 长异步工作不要持互斥锁,使用局部副本 + 版本/指针校验。
- 默认回滚,显式提交,让异常控制流自动偏向安全。
- 跨越多个状态介质时,不要假装有一个万能事务;为每种介质设计恢复机制。
本章源码索引
src-tauri/src/config/config.rs::Config:四层配置聚合src-tauri/src/config/clash.rs::IClashTemp:应用维护的 Mihomo Mappingsrc-tauri/src/config/verge.rs::IVerge:产品配置src-tauri/src/config/profiles.rs::IProfiles:Profile 索引src-tauri/src/config/runtime.rs::IRuntime:增强产物crates/clash-verge-draft/src/lib.rs::Draft:COW 快照crates/clash-verge-draft/src/lib.rs::DraftTransaction:跨层 claim/commit/rollbacksrc-tauri/src/config/snapshot.rs:磁盘文件快照与恢复