SpacetimeDB 旧版本数据兼容性测试:testdata 夹具目录的用途、结构与实现原理
2026/9/12 17:02:33 网站建设 项目流程

SpacetimeDB 旧版本数据兼容性测试:testdata 夹具目录的用途、结构与实现原理

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

导读

SpacetimeDB 是一个以"开发速度如光速"为目标的关系型实时数据库,其数据以 commitlog(提交日志)与快照(snapshot)两种形态持久化到磁盘。随着版本迭代,磁盘上的持久化格式、系统表结构都可能发生变化,因此数据库必须具备"读取旧版本写入的数据"的能力。本文以仓库中crates/engine/testdata/目录的说明文档为线索,深入解析 SpacetimeDB 如何通过"固化旧版本真实数据"的测试夹具,构建起向后兼容性回归测试体系——读完你将掌握该夹具目录的完整结构、对应单元测试的运行逻辑,以及 commitlog 格式版本、BSATN 快照存储等底层实现细节。

一、testdata 目录存在的意义:让单元测试检查版本兼容性

仓库根目录下 crates/engine/testdata/README.md 对该目录的定位只有一句话:

This has some data written by older versions of spacetimedb, so our unit tests can check compatibility.

(这里存放着由旧版本 spacetimedb 写入的一些数据,以便我们的单元测试能够检查兼容性。)

这句话概括了测试夹具(test fixture)的经典用途:把某个历史版本运行后真实产生的磁盘数据"固化"进代码仓库,当引擎后续迭代时,单元测试直接用这份"陈年数据"打开数据库,验证:

  1. 旧数据可以被当前代码正确读取(前向读取兼容);
  2. 旧数据可以被升级/迁移到新结构(如新增系统表后仍可读写);
  3. 回归防护——任何破坏旧格式读取能力的代码变更,都会在cargo test阶段立即暴露,而不是等到用户升级时才发现。

换句话说,testdata 是 SpacetimeDB 向后兼容承诺的"可执行证据"。

二、夹具目录结构逐层解析

crates/engine/testdata/下的实际数据布局如下:

crates/engine/testdata/ └── v1.2/ └── replicas/ └── 22000001/ # 数据库 replica_id = 22000001 ├── clog/ │ └── 00000000000000000000.stdb.ofs # commitlog 日志段(offsets 文件) ├── module_logs/ # 模块日志目录(当前为空) ├── db.lock # 数据库目录锁 └── snapshots/ └── 00000000000000000000.snapshot_dir/ # 偏移 0 处的快照目录 ├── 00000000000000000000.snapshot_bsatn # BSATN 编码的快照主体 └── objects/ ├── 19/30ce81246a4cdc25e9024ae0065d053adb2efbe1b5b7af457331d330e481e8 ├── 41/bb11b6d2cdc488192ee70d8175307d6f205756ed163f4237c6cba2936798dc ├── 45/4d2e2c62ff5d46c5b3e6de72d6277eb285fc2d6b0a5ac6f92498e08a9e5ecc ├── 62/22df0e5ca93d3fb22762e12161246a1d5917c61ada5d81b8dcce12fd5780b3 ├── 79/4dced5633eca2ffee784d471f5203209169321083ef99de254ad24af0f6d5a ├── 95/74dd6d2857fa771a1cd16be31fdef38f83c2fd3bcc05f4934e53bdbfa21f10 └── 9a/b95f5aaed7541289faa8bc4de886ce0281f11037c3424494e58fee92411241

这份布局与 crates/paths/src/lib.rs 中定义的服务器数据目录规范完全一致:每个数据库副本以replicas/<replica_id>组织,内部包含clog(CommitLog 文件)、module_logs(模块日志)、snapshots(数据库快照)。对应地,crates/paths/src/server.rs 的ServerDataDir::replica(replica_id)与 ReplicaDir 的路径访问器 分别提供了snapshots()commit_log()等类型安全路径构造方法。

各组成部分的含义:

路径含义
clog/00000000000000000000.stdb.ofscommitlog 的日志段文件(.ofs后缀),记录从事务偏移 0 开始的所有已提交事务
snapshots/00000000000000000000.snapshot_dir/在事务偏移 0 处生成的一个快照目录,snapshot_dir(tx_offset)的命名规则为{tx_offset:0>20}.snapshot_dir(见 server.rs)
00000000000000000000.snapshot_bsatn快照主体,采用 SpacetimeDB 的 BSATN 二进制序列化格式编码
objects/对象存储目录树(按哈希前两字节分片),存放表中实际数据的不可变对象文件
db.lock数据库目录互斥锁,防止同一副本被并发打开
module_logs/模块(业务逻辑)运行日志目录,当前夹具中为空

objects/采用内容寻址(content-addressed)存储:每个数据对象以内容哈希命名(如19/30ce8124...),前缀两字符作为分片目录,形成两级目录树。这种设计让快照天然支持去重与硬链接——下文会看到压缩逻辑如何利用这一点。

三、四个兼容性单元测试:验证了什么

测试数据不是摆设,crates/engine/src/relational_db.rs 的测试模块中有 4 个用例直接消费这份 v1.2 夹具,核心逻辑收敛在两个函数中。

3.1 load_1_2_data:读旧数据 + 建新表

load_1_2_data(use_snapshot: bool)的流程(源码):

let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/v1.2/replicas"); let tempdir = copy_fixture_dir(&data_dir); // 先复制到临时目录,绝不直接改动仓库内的夹具 let dir = ReplicaDir::from_path_unchecked(tempdir.path().join("replicas/22000001")); // ... 使用固定 identity 打开已存在的持久化数据库 ... let (db, _durability_handle) = TestDB::open_existing_durable(&dir, ...)?; // 1) 断言旧数据表存在 let schemas = db.get_all_tables_mut(&tx)?; let expected_table_names = vec!["message", "user"]; // v1.2 时代的数据表 assert_eq!(user_table_names, expected_table_names); // 2) 在旧数据之上新建一张表并提交 let table_id = db.create_table(&mut tx, my_table(AlgebraicType::I32))?; db.commit_tx(tx)?; // 3) 断言新表读写正常 assert_eq!(table_id, db.table_id_from_name_mut(&tx, "MyTable")?.unwrap());

关键点:

  • env!("CARGO_MANIFEST_DIR")在编译期定位 crate 根目录,从而找到 testdata 夹具;
  • copy_fixture_dir先把夹具整体复制进临时目录(定义在 relational_db.rs#L4023-L4037),保证测试对夹具文件本身零污染;
  • 断言夹具中确实含有messageuser两张 v1.2 时代的数据表,先确认"没有打开空目录",再验证旧数据可读、新表可建。

3.2 load_1_2_data_and_migrate:系统表迁移的完整闭环

load_1_2_data_and_migrate(use_snapshot)(源码)更进一步,模拟了一次真实的升级迁移场景:

This tests adding a new system table st_connection_credentials, which was not in 1.2.

  1. 打开 v1.2 旧数据,再次断言messageuser存在;
  2. 通过insert_st_client(Identity::ZERO, ConnectionId::ZERO, "invalid_jwt")1.2 版本中不存在的系统表st_connection_credentials写入数据——触发当前代码自动创建缺失系统表并完成迁移;
  3. rt.block_on(db.shutdown())关闭并drop(db)重新打开数据库;
  4. get_jwt_payload(ConnectionId::ZERO)读回刚才写入的 JWT 字符串,断言jwt == "invalid_jwt"

这个用例验证了迁移的持久性:新系统表不仅能在旧数据上被创建,而且写入的数据在关闭重开后依然能正确读取。这正是用户从旧版本升级后不会丢身份凭据的关键保证。

3.3 测试矩阵

4 个测试分别以"有无快照"两种路径执行(测试声明):

测试函数覆盖场景快照路径
load_1_2_quickstart_from_snapshot_test读旧数据 + 建新表从快照恢复
load_1_2_quickstart_without_snapshot_test读旧数据 + 建新表纯 commitlog 重放
load_1_2_data_and_migrate_with_snapshot系统表迁移 + 重开读回从快照恢复
load_1_2_data_and_migrate_without_snapshot系统表迁移 + 重开读回纯 commitlog 重放

use_snapshot布尔值通过TestDB::open_existing_durable(..., want_snapshot_repo)控制(signature),因此同一个夹具覆盖了两条完全不同的恢复链路:快照 + 增量日志 vs 全量日志重放。这是"一份数据,两条路径"的高性价比测试设计。

四、底层原理:快照与 commitlog 的格式版本

4.1 快照:BSATN 序列化 + 内容寻址对象存储

快照的抓取由 crates/engine/src/snapshot.rs 中的SnapshotWorker后台任务负责。它是一个可克隆的句柄,通过mpsc无界通道接收Request::TakeSnapshot/Request::ReplaceState消息(Request 枚举),在 tokio runtime 上用spawn_blocking调用Locking::take_snapshot_internal完成内存状态到磁盘的落盘(take_snapshot),最后fsync并发布快照对应的事务偏移(TxOffset)。

快照主体00000000000000000000.snapshot_bsatn使用 SpacetimeDB 自研的BSATN(Binary Spacetime Algebraic Type Notation)格式编码,表中数据对象则存进objects/内容寻址目录。SnapshotWorker还支持Compression::Enabled压缩模式:对早于最新快照的历史快照执行压缩,统计跳过数、压缩对象数、硬链接对象数(CompressionMetrics)——"硬链接"正是利用内容寻址哈希去重,让多个快照共享同一份对象文件,从而节省磁盘。

4.2 commitlog:带版本号的日志格式

与快照平行的持久化通道是 commitlog。日志段文件(.stdb.ofs)在解码事务时会读取log_format_version字段,crates/commitlog/src/commit.rs 中的逻辑表明:格式版本0对应Version::V0,其余版本走Version::V1解码路径。

该版本号可通过服务器配置覆盖,crates/engine/src/persistence.rs 的CommitlogConfig暴露了log_format_versionmax_segment_sizeoffset-index-interval-bytespreallocate-segmentswrite-buffer-size等 kebab-case 配置项。这解释了兼容性测试的意义:同一份 commitlog 可能由不同格式版本的引擎写入,解码端必须按段内自带的版本信息选择正确的解析逻辑。

4.3 持久化装配:LocalPersistenceProvider

生产环境中,crates/engine/src/persistence.rs 的LocalPersistenceProvider会把上面两条链路装配起来:为每个副本打开快照仓库并创建启用了压缩的SnapshotWorker,构建本地Durability(commitlog),再后台运行"快照驱动的 commitlog 压缩器"——每当新快照产生,就压缩对应的旧日志段。测试夹具中同时出现的.snapshot_bsatn.stdb.ofs文件,正是这套装配的磁盘产物。

五、实战指引:如何运行与扩展这些测试

5.1 运行兼容性测试

在仓库根目录执行(需要 Rust 工具链,工具链版本见根目录 rust-toolchain.toml):

# 只运行 v1.2 兼容性相关测试 cargo test -p spacetimedb-engine load_1_2 # 运行 engine crate 全部测试 cargo test -p spacetimedb-engine

由于夹具会先复制到临时目录,测试可重复执行且不会污染crates/engine/testdata/中的原始数据。

5.2 为新版本添加夹具(基于现有模式的推断)

从当前仓库的约定可以推断,维护者为新版本(如 v1.3)添加兼容性夹具时,遵循的流程会是:

  1. 用对应旧版本引擎实际创建并写入一个数据库(含messageuser等代表性业务表);
  2. 正常关闭(确保 commitlog 与快照落盘),将整个replicas/<id>目录固化为testdata/vX.Y/replicas/
  3. 在 relational_db.rs 测试模块中仿照load_1_2_data新增读取与迁移断言。

这种"真实数据固化 + 双重打开验证"的模式,保证了每一条持久化格式变更都有对应的历史回归用例。

六、结语

crates/engine/testdata/虽然只有一行说明,却承载着 SpacetimeDB 最核心的工程承诺——向前兼容。通过固化 v1.2 时代的真实磁盘数据(commitlog 日志段、BSATN 快照、内容寻址对象存储),配合四个覆盖"快照/日志重放 × 读取/迁移"矩阵的单元测试,任何破坏旧格式读取或系统表迁移的改动都无法通过 CI。对于数据库类项目,这套"以历史真实数据为测试基准"的方法论本身,就值得借鉴。

延伸阅读

  • 测试实现:crates/engine/src/relational_db.rs#L4039-L4174
  • 数据目录布局规范:crates/paths/src/lib.rs#L129-L154
  • 快照后台任务:crates/engine/src/snapshot.rs
  • 持久化配置(含 commitlog 格式版本):crates/engine/src/persistence.rs
  • commitlog 解码与格式版本:crates/commitlog/src/commit.rs#L302-L314
  • 夹具数据本体:crates/engine/testdata/v1.2/replicas/22000001

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询