
Lance 数据完整性实战fri_straddle_pre_6610 测试夹具与 FRI 跨重写组索引损坏的复现、检测与自愈【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lance导读fri_straddle_pre_6610是 Lance 仓库中一个专门保存损坏状态的回归测试数据集fixture它忠实记录了 PR #6610 修复之前、由一次并发提交竞态导致的真实磁盘损坏一个用户索引的fragment_bitmap跨越了 fragment-reuse 重写组FRI-straddle。本文以 test_data/fri_straddle_pre_6610/README.md 为主线结合 datagen/src/main.rs、rust/lance/src/index.rs 等源码讲清损坏的成因、磁盘上的表现形态、Rust 测试如何用它验证validate()与自愈逻辑以及如何在修复前的版本上确定性再生该夹具。读完你可以理解 Lance 索引与 fragment-reuse 之间的内在约束并掌握一套构造损坏数据 → 验证检测 → 验证修复的回归测试方法。一、FRI-straddle 损坏问题背景与症状1.1 什么是 FRIFragment Reuse IndexLance 在索引与数据文件之间通过fragment reuse indexFRI建立映射当压缩compaction以重写组rewrite group的形式合并若干 fragment 时索引并不会立刻重建而是借助 FRI 记录旧 fragment 的哪一段地址现在落在哪个新 fragment 里从而在查询时把旧 row address 重新映射到新数据。这一点在 rust/lance/src/dataset/optimize.rs 的defer_index_remap字段文档中有明确说明Whether to defer remapping indices during compaction. If true, indices will not be remapped during this compaction operation. Instead, the fragment reuse index is updated and will be used to perform remapping later.optimize.rs也就是说defer_index_remaptrue时压缩只写 FRI把索引重映射推迟到后续某次提交来完成。这本是一条性能优化路径但一旦与其他并发写入交错就可能产生不一致。1.2 损坏的完整成因链该 fixture 对应的损坏状态由 PR #6610 修复成因是两笔并发提交在过期句柄上互相覆盖数据集中存在已建立向量索引的 fragmentfrag0追加 frag1 后代码克隆出一个stale过期dataset 句柄在最新句柄上继续追加 frag2随后用defer_index_remaptrue规划并执行 frag1frag2 的压缩plan_compaction→CompactionTask::execute但先不提交与此同时stale 句柄它根本不知道 frag2 的存在执行optimize_indices提交一个只覆盖 frag1 的 CreateIndex最后才提交那笔 deferred-remap 压缩。在修复前的构建里冲突解析器把这两笔事务判定为兼容于是损坏状态被写入磁盘该用户索引的fragment_bitmap只覆盖重写组的一部分跨越了 fragment-reuse 重写组边界。1.3 症状list_indices()直接 panic修复前的构建上打开该数据集并调用Dataset::list_indices()会直接 panic报错信息为The compaction plan included a rewrite group that was a split of indexed and non-indexed data这正是 README.md 中描述的症状索引声称覆盖某些 fragment但这些 fragment 已被重写组劈开导致索引加载逻辑无法自洽。二、PR #6610 修了什么冲突解析器的一行分支要理解为什么修复前会把损坏写盘需要看提交冲突解析器。修复前(None, Some(_)) Ok(())这一分支把对方有、我无的冲突错误地判定为兼容修复后这类与未提交重写相互冲突的提交会以RetryableCommitConflict可重试提交冲突被拒绝由调用方重新规划后重试而不是静默写坏数据。在当前仓库中这一拒绝行为可以在 rust/lance/src/dataset/index/frag_reuse.rs 附近看到——当检测到冲突时返回Error::RetryableCommitConflictoptimize.rs 的注释也明确指出当并发写触及相同 fragment 时提交必须以可重试冲突错误结束。这是损坏被杜绝与损坏被写出的分水岭。一个实用的验证技巧因为冲突被判定为 retryable所以修复后的构建无法再生成该 fixture——commit_compaction会失败并报错这正是再生脚本所依赖的前向兼容信号见第五节。三、夹具的磁盘结构一份真实的损坏 manifestfixture 本体位于 test_data/fri_straddle_pre_6610/fri_straddle_dataset是一份真正落在磁盘上的损坏数据集目录结构如下fri_straddle_dataset/ ├── _indices/ │ ├── 9026cf25-1429-435c-88b1-760f41784b50/ │ │ ├── auxiliary.idx │ │ └── index.idx │ └── bd127c0c-fd1a-4c2a-aa2f-3336b3f38f4e/ │ ├── auxiliary.idx │ └── index.idx ├── _transactions/ │ └── 4-13a028b0-0511-4725-ad48-ae463f2c936a.txn ├── _versions/ │ └── 18446744073709551608.manifest └── data/ ├── 0111000011001001001100012f5d8b4bfe8dc9b1dcefba238e.lance └── 1101010010001101110110003e36734fffa25e4ce82d350e11.lance几个值得注意的细节两个索引目录fixture 中保留了两份索引数据auxiliary.idxindex.idx成对出现对应向量索引的辅助元数据与主索引文件manifest 版本号18446744073709551608接近u64::MAX这是再生脚本最后执行cleanup_old_versions清理中间版本后保留的最新 manifest——再生逻辑刻意把中间版本全部清掉只保留损坏状态对应的那一版以减小 fixture 体积见 datagen/src/main.rs两个.lance数据文件对应重写组涉及的旧 fragment 数据。损坏 manifest 的历史可能引用一些从未真正落盘的重写产物因此清理旧版本时可能出现NotFound再生脚本对此做了 best-effort 处理清理失败只打印日志不中断生成。四、Rust 测试如何消费这份损坏数据该 fixture 的价值在于它让检测损坏和验证修复可以跑在真实磁盘 manifest 上而不是内存里构造的假数据。当前仓库中与它直接相关的测试位于 rust/lance/src/index.rs。4.1 容错加载test_load_indices_tolerates_fri_straddle该测试index.rs把 fixture 复制到临时目录后直接打开数据集并断言load_indices()返回Ok且索引列表非空修复后的构建不再 panic紧接着dataset.validate()也能成功。注释说明修复后的语义是加载时对旧 fragment ID做容错剔除——受影响的老 fragment ID 被丢弃不向 FRI 中插入任何新 fragment ID从而让validate()通过。也就是说加载路径本身承担了第一层修复。4.2 自愈并持久化test_auto_heal_persists_cleaned_bitmap第二层修复更关键index.rs。该测试验证先直接读取磁盘 manifest 里的原始索引read_manifest_indexes确认 fixture 中确实存在fragment_bitmap需要被清洗的索引段执行load_indices()得到清洗后的索引断言两者的fragment_bitmap确实不同然后执行一次无实际删除的 no-op 写dataset.delete(false)——由于任何提交都会经由load_indices → build_manifest重新播种索引这一次空提交就把清洗后的 bitmap持久化回磁盘重新打开数据集断言磁盘上的原始 bitmap 已经与清洗后的一致。这揭示了一个优雅的自愈模型不需要专门的 repair 命令任何一次正常提交都会顺带把损坏的索引 bitmap 修好并落盘。README 中提到的Dataset::validate()错误报告与Dataset::repair()验证正是围绕这一机制组织的回归场景。4.3validate()的校验语义Dataset::validate()rust/lance/src/dataset.rs的检查项与这个 fixture 高度相关所有 fragment ID 唯一、并按 fragment ID 递增排序每个 fragment 内部自校验stable row id 校验加载全部索引并做validate_indices检查索引 UUID 不重复、覆盖范围不重叠——注释明确说明这些检查针对的是manifest 说了什么与当前构建能否读取该索引无关且migrate_indices在每次提交时都会执行同样的重叠检查。这意味着fragment_bitmap的重叠/跨组问题正是validate()与提交路径共同防御的对象而 FRI-straddle fixture 恰好提供了命中这条防御的真实样本。五、为什么必须用 Rust 再生Python 到不了这条路径README 明确指出再生是确定性的但仅限于 Rust 侧原因有二Rust 可以直接驱动底层三件套plan_compaction规划、CompactionTask::execute执行重写、commit_compaction提交并且可以精确地在规划完成、尚未提交的窗口里插入optimize_indices见 datagen/src/main.rs。时序完全受控无需碰运气。Python 的Compaction.commit硬编码了默认选项它内部使用CompactionOptions::default()而默认值里defer_index_remap为falseoptimize.rs根本没有 defer-remap 路径因此 Python 生成器只能在极紧的提交竞态下偶尔撞上无法确定性复现。生成器是一个刻意独立于父 Cargo workspace 的 cratedatagen/Cargo.toml注释里写得很直白Standalone — deliberately not part of the parent Cargo workspace. This crate must compile against a pre-#6610 build oflance, where the conflict resolver still has the buggy(None, Some(_)) Ok(())arm.它把lance、lance-datagen、lance-index、lance-linalg全部固定到v6.0.0-beta.3——这是 PR #6610 合并前的最后一个发布标签。若把它加入父 workspace就会用修复后的代码编译反而无法复现 bug。5.1 生成器的关键步骤拆解datagen/src/main.rs 的完整时序可以归纳为五步步骤操作代码位置1写 256 行、16 维随机向量作为 frag0创建 IVF-PQ 向量索引main.rs2追加 frag164 行克隆出stale句柄main.rs3在最新句柄上追加 frag264 行main.rs4用defer_index_remaptrue规划并执行 frag1frag2 的重写但不提交main.rs5用 stale 句柄执行optimize_indices只覆盖 frag1随后提交重写main.rs生成的向量索引参数VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 50)对应 IVF 分区数 2、PQ 子向量 8、每子向量 bit 数 2、L2 距离、训练样本数 50。5.2 再生命令在修复前pre-#6610的构建环境中执行cargo run --release \ --manifest-path test_data/fri_straddle_pre_6610/datagen/Cargo.toml -- \ test_data/fri_straddle_pre_6610/fri_straddle_dataset注意两个细节由于datagen不在父 workspace 内必须显式指定--manifest-path指向 datagen/Cargo.toml若输出目录已存在脚本会先整体删除再重建见 main.rs。5.3 在修复后的构建上会大声失败这正是该生成器作为回归保障的精妙之处在已修复的构建上运行它会失败而且失败是预期行为。因为commit_compaction会被冲突解析器以RetryableCommitConflict拒绝生成器会把错误包装为commit_compaction failed: {e}. This generator must be run on a Lance build without PR #6610 applied.main.rs——它用一次失败的运行宣告这条损坏路径已经不存在了。5.4 升级固定标签的约束README 对标签升级给出了严格限制datagen/Cargo.toml中的固定标签只能升级到另一个同样早于 #6610 的构建绝不能随手 bump 到修复后的版本否则生成器将失去复现能力。升级后应重新运行生成命令确认仍能产出损坏 fixture。六、使用前提与注意事项修复前 vs 修复后的行为差异是核心语义损坏数据集只有在修复后的 Lance 上打开才能被容错加载并自愈在修复前打开则会触发list_indices()panic。不要在同一构建上既期望复现又期望自愈。defer_index_remap有适用限制源码显示defer_index_remaptrue不适用于启用 stable row ids 的数据集optimize.rs规划压缩时也会对不支持的组合直接报错。该 fixture 基于旧的 row-address 体系构造。配置入口defer_index_remap除了在代码中设置CompactionOptions字段还对应配置键lance.compaction.defer_index_remapoptimize.rs可在相关会话配置中控制。fixture 只读仓库中的 fri_straddle_dataset 是检查进仓库的成品测试运行时通过copy_test_data_to_tmp复制到临时目录再打开见 index.rs避免污染仓库内数据。结语fri_straddle_pre_6610是用真实损坏数据做回归测试的范本它把一次并发竞态固化为磁盘上的确定状态让load_indices容错、validate()报告、提交自愈三个环节都能在真实 manifest 上被反复验证而它的再生器又通过固定在 bug 版本上和在修复版本上大声失败两头夹击确保该损坏路径既可以被复现、又永远不会被悄悄带回。对任何关心 Lance 数据完整性、索引一致性与压缩并发语义的开发者来说这份 fixture 连同 datagen/src/main.rs 都是值得精读的参考实现。【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lance创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考