Marionette 用于 Zig 的确定性 I/O 和仿真测试。
从长远来看,Marionette 的目标是成为 Zig 的确定性 std.Io 标准:生产环境库采用 std.Io,而在测试中则替换为 Marionette 的确定性实现。如今,Marionette 提供了仿真器、追踪(trace)、故障注入、磁盘和网络原语,使这一方向成为现实。Marionette 已经在确定性仿真环境下运行了真实的、未经修改的 Zig 代码:一个存储引擎 (xit-vcs/xitdb) 和一个协作式并发库 (g41797/mailbox),两者均支持基于种子(seed)的可复现回放,并在过程中发现了可复现的恢复问题及正确性反例。
针对 std.Io 以及 Marionette 实际需要的少量句柄(如 mar.Recorder 或 mar.Endpoint(Message))编写生产环境代码。在测试中,驱动控制逻辑以注入故障。对于所模拟的文件和本地端点接口,同一套应用程序逻辑既可以在仿真器上运行,也可以在生产适配器上运行。
fn writeAndRecover(io: std.Io, root: std.Io.Dir, recorder: mar.Recorder) !KVStore {
var store = try KVStore.init(io, root, recorder);
try store.put(1, 41, .sync);
try store.put(2, 99, .no_sync);
try store.recover(.strict);
return store;
}
// 在仿真中:确定性、可注入故障、可基于种子回放。
const sim = try world.simulate(.{ .disk = .{ .sector_size = 16 } });
var sim_store = try writeAndRecover(sim.env.io(), std.Io.Dir.cwd(), sim.env.recorder());
// 在生产中:真实磁盘,相同的代码路径。
var production = try mar.Production.init(.{ .root_dir = tmp.dir, .io = std.testing.io });
const prod_env = production.env();
var prod_store = try writeAndRecover(prod_env.io(), tmp.dir, prod_env.recorder());
对于此类基于文件的代码,这种对等性正是关键所在。你无需编写“仿真版”代码。你只需在 Marionette 拥有的权威接口后编写业务逻辑,Marionette 便会为你提供一个运行它的确定性环境。
为什么选择它?
分布式和存储系统往往以难以复现的方式失败:崩溃时的碎片化写入、法定人数(quorum)期间的网络分区、两个计时器之间的竞态。当你拿到堆栈跟踪(stack trace)时,导致错误的条件早已消失。
确定性仿真测试将这些 bug 变成了种子(seed)。每次运行都是可复现的。每次失败都是可回放的。你可以将数周的模糊测试压缩至几秒钟,而当 CI 中出现故障时,仅凭种子就足以进行调试。
Marionette 将这一方法引入了 Zig。它的灵感源于 FoundationDB、TigerBeetle 和 Antithesis 背后的技术,但被设计为一个即插即用的库,而不是一个你需要围绕它构建系统的框架。
完整示例
这是一个 WAL 恢复测试,它在写入过程中使磁盘崩溃,损坏扇区,并断言已提交的记录能够存活,而未同步的记录则不会。
pub fn scenario(harness: *Harness) !void {
try harness.store.put(committed_key, committed_value, .sync);
try harness.control.disk.setFaults(.{ .crash_lost_write_rate = .always() });
try harness.store.put(volatile_key, volatile_value, .no_sync);
try harness.control.disk.crash();
try harness.control.disk.restart();
try harness.control.disk.corruptSector(wal_path, record_size);
try harness.store.recover(.strict);
}
pub const checks = [_]mar.StateCheck(Harness){
.{ .name = "同步记录恢复,非同步记录被拒绝", .check = recoveredStateIsSafe },
};
test "wal recovery" {
try mar.expectPass(.{
.allocator = std.testing.allocator,
.seed = 0xC0FFEE,
.init = Harness.init,
.scenario = scenario,
.checks = &checks,
});
}
每个测试包含三个部分:
- init:设置你的测试工具(harness),包括待测代码和用于故障注入的控制句柄。
- scenario:驱动动作。通过
env 创建的句柄调用代码,并通过 control 调用仿真器。
- checks:断言最终状态的不变性。
两个层面:io 和 control
每个 Marionette 测试都有两个部分:
- io:生产环境存储代码通常应看到的接口。在仿真中,
sim.env.io() 返回 Marionette 的确定性 std.Io 后端。在生产中,production.env().io() 返回设置时提供的宿主 std.Io。
- control:测试代码用于注入故障的接口。它仅在仿真中可用。它镜像了
env 的结构:每个资源都有一个控制表面。
分布式仿真
网络仿真以同样的方式工作。以下是一个针对小型复制寄存器的分区测试:
try harness.control.network.partition(&isolated, &majority);
try harness.replicas.write(.{ .version = 1, .value = 41, .retry_limit = 2 });
try harness.control.network.heal();
协作式并发
Marionette 还拥有一个实验性的、基于调度器的 std.Io futex 路径,用于协作式 Mutex / Condition 代码。它支持计时接收、发送/唤醒、相同截止时间的超时排序以及字节级相同的种子回放。
追踪 (Traces)
每次运行都会产生结构化追踪。当检查失败时,你将获得导致违规的完整事件序列,以及用于复现它的种子。
状态
Marionette 尚处于早期阶段。这是一个 0.x 版本:在 1.0 之前没有 API 稳定性保证。
安装
zig fetch --save https://github.com/sb2bg/marionette/archive/<commit>.tar.gz
需要 Zig 0.16.x。
许可协议
MIT
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组
从长远来看,Marionette 的目标是成为 Zig 的确定性
std.Io标准:生产环境库采用std.Io,而在测试中则替换为 Marionette 的确定性实现。如今,Marionette 提供了仿真器、追踪(trace)、故障注入、磁盘和网络原语,使这一方向成为现实。Marionette 已经在确定性仿真环境下运行了真实的、未经修改的 Zig 代码:一个存储引擎 (xit-vcs/xitdb) 和一个协作式并发库 (g41797/mailbox),两者均支持基于种子(seed)的可复现回放,并在过程中发现了可复现的恢复问题及正确性反例。针对
std.Io以及 Marionette 实际需要的少量句柄(如mar.Recorder或mar.Endpoint(Message))编写生产环境代码。在测试中,驱动控制逻辑以注入故障。对于所模拟的文件和本地端点接口,同一套应用程序逻辑既可以在仿真器上运行,也可以在生产适配器上运行。对于此类基于文件的代码,这种对等性正是关键所在。你无需编写“仿真版”代码。你只需在 Marionette 拥有的权威接口后编写业务逻辑,Marionette 便会为你提供一个运行它的确定性环境。
为什么选择它?
分布式和存储系统往往以难以复现的方式失败:崩溃时的碎片化写入、法定人数(quorum)期间的网络分区、两个计时器之间的竞态。当你拿到堆栈跟踪(stack trace)时,导致错误的条件早已消失。
确定性仿真测试将这些 bug 变成了种子(seed)。每次运行都是可复现的。每次失败都是可回放的。你可以将数周的模糊测试压缩至几秒钟,而当 CI 中出现故障时,仅凭种子就足以进行调试。
Marionette 将这一方法引入了 Zig。它的灵感源于 FoundationDB、TigerBeetle 和 Antithesis 背后的技术,但被设计为一个即插即用的库,而不是一个你需要围绕它构建系统的框架。
完整示例
这是一个 WAL 恢复测试,它在写入过程中使磁盘崩溃,损坏扇区,并断言已提交的记录能够存活,而未同步的记录则不会。
每个测试包含三个部分:
env创建的句柄调用代码,并通过control调用仿真器。两个层面:io 和 control
每个 Marionette 测试都有两个部分:
sim.env.io()返回 Marionette 的确定性std.Io后端。在生产中,production.env().io()返回设置时提供的宿主std.Io。env的结构:每个资源都有一个控制表面。分布式仿真
网络仿真以同样的方式工作。以下是一个针对小型复制寄存器的分区测试:
协作式并发
Marionette 还拥有一个实验性的、基于调度器的
std.Iofutex 路径,用于协作式Mutex/Condition代码。它支持计时接收、发送/唤醒、相同截止时间的超时排序以及字节级相同的种子回放。追踪 (Traces)
每次运行都会产生结构化追踪。当检查失败时,你将获得导致违规的完整事件序列,以及用于复现它的种子。
状态
Marionette 尚处于早期阶段。这是一个 0.x 版本:在 1.0 之前没有 API 稳定性保证。
安装
zig fetch --save https://github.com/sb2bg/marionette/archive/<commit>.tar.gz需要 Zig 0.16.x。
许可协议
MIT
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: