Skip to content

第 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

普通配置写法常是:

text
修改内存 → 写文件 → 通知其他模块

但这里修改之后可能还要生成 runtime、让 Mihomo 校验、重启核心、设置系统代理。任何一步失败都不能让全局读者看到“已经生效”的值。

Draft<T> 因此保存:

rust
(committed_snapshot, optional_draft_snapshot)

对应三种操作:

操作含义
edit_draft从 committed 延迟复制并修改候选值
apply将 draft 提升为 committed
discard放弃候选值,保留 committed

三、data_arclatest_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:

text
没有其他读者 → 直接取得可变引用
仍有旧快照读者 → clone T,旧读者继续看原值

这解决了两个问题:

  1. 大型 IVerge / IProfiles 不必每次读取都 clone;
  2. 异步任务可以持有稳定快照,不受后续 patch 影响。

额外的 Box 让 Arc 内保存固定大小指针,减小外层结构尺寸;真正收益主要仍来自 Arc 快照和 COW。

五、短修改与长异步修改不是同一条路径

5.1 edit_draft:同步闭包

锁只覆盖从 Arc 取出 draft、make_mut 和闭包修改。闭包不能 await,避免持锁跨异步边界。

5.2 with_data_modify:异步所有权更新

某些 Profile 更新需要下载、读写文件。它使用不同策略:

  1. 原子标志 + Notify 串行化长修改;
  2. clone committed 到局部所有权;
  3. 异步闭包在无锁状态执行;
  4. 返回后比较 committed Arc 指针;
  5. 若期间被其他提交替换,报 optimistic lock failed;
  6. 否则原子替换 committed。

这是一种轻量乐观并发控制。它避免长时间占用 Mutex,又不会让晚到的异步结果覆盖新值。

六、单层 Draft 还不够

修改端口会同时影响:

  • IClashTemp:Mihomo listener;
  • IVerge:界面选择和 enabled 状态;
  • IRuntime:最终运行时 YAML。

若三层分别 apply,第二层失败时第一层已经提交,系统就会自相矛盾。

DraftTransaction 接收不同类型的 DraftLayer

rust
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.yamlverge.yaml 和 runtime 文件,内存 rollback 不会把文件恢复。

项目用 config/snapshot.rs 捕获:

  • 文件原内容;
  • 文件原本是否存在。

失败时先 transaction.rollback(),再 restore_files()。顺序不能反:文件恢复或重新生成会读取 committed,必须先保证内存已经回到旧值。

八、配置状态的五个时间点

一次可靠 patch 可以分成:

text
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。理解这一点后,项目中看似“重复”的恢复代码就有了共同模型。

十、可迁移的设计原则

  1. 把候选状态做成一等概念,不要用注释约定“这个对象暂时还没生效”。
  2. 读 API 要表达语义committed()latest() 比一个含糊的 get() 更安全。
  3. 长异步工作不要持互斥锁,使用局部副本 + 版本/指针校验。
  4. 默认回滚,显式提交,让异常控制流自动偏向安全。
  5. 跨越多个状态介质时,不要假装有一个万能事务;为每种介质设计恢复机制。

本章源码索引

  • src-tauri/src/config/config.rs::Config:四层配置聚合
  • src-tauri/src/config/clash.rs::IClashTemp:应用维护的 Mihomo Mapping
  • src-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/rollback
  • src-tauri/src/config/snapshot.rs:磁盘文件快照与恢复

原创文档 CC BY-SA 4.0 · 站点代码 MIT