Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

R6 Design — Failpoint crash testing + G7 fencing exam harness

日期:2026-08-18 · 状态:设计 + crate 脚手架(未接入写路径,未跑 10000 seeds) 对应 ROADMAP-2026-08-10 §R6。Porcupine / fixture 已关(crates/correctness + scripts/porcupine_check.sh)。不要重做。 本文件管剩余:六处 crash 点、seed 1..N 重启环、以及 G7 fencing 考试的 harness 设计(活门在 R2,本轮只设计不跑)。


0. 本轮交付 vs 以后

本轮以后(别的会话 / 别的 owner)
本文把 failpoint! 编进 writer.rs / chunk_fs.rs / compact.rs / gc
crates/swift-failpoints/ 库 + 单测:env 对上才 trip10000-seed 真跑(集群、会杀 daemon)
harness 环的文字合同R2 落地后再跑 G7 fencing 考试
不改 crates/correctness、不改 porcupine 脚本

writer.rs / etcd.rs 本轮不插宏(避免和 R2 / 写路径 owner 打架)。


1. 不变量(崩溃只许留 orphan)

宗旨第三条:block PUT 先于 metadata CAS。崩溃之后可见状态只能是:

  1. CAS 没成功:inode JSON / 名 key 与崩溃前相同。Swift 上多出来的对象是 orphan(无指针)。读、stat、readdir 不得看见半截文件。
  2. CAS 成功、回复没发出:元数据已是新 version。重启后 getattr/read 必须看到完整新图。内核可能重发 FUSE 写/fsync:必须幂等(同一 slice 对象名再 PUT,或 CAS 发现 version 已新)。
  3. 禁止:名已指向新 inode 但 inode JSON 是旧的;inode JSON 指向还没 PUT 的对象;dir listing 有名但 LOOKUP ENOENT(除非 POSIX 允许的并发窗口,failpoint 环里是单线程写)。

Compaction / GC 同构:

  • compaction:新对象 PUT 成功、CAS 没换指针 → 新对象 orphan,旧切片仍被引用。读必须仍是旧字节。
  • GC:DELETE 前崩溃 → 对象仍在,只是泄漏。DELETE 后崩溃 → 对象已无引用才允许消失。

2. 六个点(名字即合同)

宏名与 ROADMAP 一致。每个名字是 SWIFTFUSE_FAILPOINT 的精确匹配值。

常量以后插在哪(本轮不插)崩溃时已发生重启后必须
FAILPOINT_AFTER_BLOCK_PUTChunkWriter::flush:upload_blocks 成功之后、返回给 flush_handle 之前Swift 有新对象;etcd 仍是旧 chunk map读旧数据;新对象无引用(orphan)
FAILPOINT_BEFORE_METADATA_CASflush_handle:组好 meta 之后、commit_inode 之前同上(PUT 已完成)同上
FAILPOINT_AFTER_METADATA_CAScommit_inode 返回 Committed 之后、改 Handle / invalidate / FUSE 回复之前etcd 已是新 version重启后读新数据;重放 fsync 不得损坏
FAILPOINT_BEFORE_REPLYFUSE 写路径:本地 invalidate 之后、Ok(Reply*) 回到 fuse3 之前(flush/fsync/release)元数据已提交同 AFTER_CAS;内核会当没收到回复
FAILPOINT_COMPACTION_AFTER_PUTCompactor::compact_chunk:upload_blocks 之后、commit_inode 之前新 compacted 对象在 Swift;旧切片仍被引用读仍等于 snapshot;新对象 orphan
FAILPOINT_GC_BEFORE_DELETEgc_blocks / store.delete_blocks 之前inode 名已删(或 deferred-GC 已摘名);对象还在名不可见;对象可留(泄漏,不是损坏)

BEFORE_METADATA_CAS 和 AFTER_BLOCK_PUT 在 flush 上几乎同一窗口。两个都要:以后 PUT 和 CAS 之间若插入 lease/fencing 检查(R2),两点会分开。脚手架先都留着。

etcd.rs 的 commit_inode 不要作为第一插点:R2 拥有该文件;CAS 前后的点放在 调用方(flush_handle / setattr 循环 / compact)。


3. Crate API(已脚手架)

路径:crates/swift-failpoints/。包名 swift-failpoints。

未写入根 Cargo.toml members(该文件和其他 agent 抢)。以后一行:

"crates/swift-failpoints",

接入写路径时再:

# crates/chunk-plane/Cargo.toml 和/或 crates/swift-fuse/Cargo.toml
swift-failpoints = { path = "../swift-failpoints" }

3.1 行为

failpoint!(NAME)
  • 默认 空操作(零分配,读两三个 env)。
  • 同时满足才 trip:
    • SWIFTFUSE_FAILPOINT 等于 NAME(不是逗号列表,不是前缀);
    • SWIFTFUSE_FAILPOINT_SEED 非空(harness 写入的 seed 十进制串)。
  • 默认 trip:std::process::abort()(不跑 drop,模拟 SIGABRT/kill -9)。
  • 测试:set_hook(fn(&str)) 替代 abort。单测用 hook,不 abort 测试进程。

SWIFTFUSE_FAILPOINT_SEED 不参与 “是否 trip” 的哈希。它是 这次崩溃的身份:harness 重启后还能说 “seed 4291 死在 AFTER_BLOCK_PUT”。进程内不必读 seed 来做概率。10000 个 seed = 10000 次 独立 的 “设 env → 打一枪 → 杀 → 起 → 查”。

不要静默的 hash(seed) % 10000 == 0。那种测不完、也复现不了。

3.2 表面(lib.rs)

#![allow(unused)]
fn main() {
pub const FAILPOINT_AFTER_BLOCK_PUT: &str = "FAILPOINT_AFTER_BLOCK_PUT";
pub const FAILPOINT_BEFORE_METADATA_CAS: &str = "FAILPOINT_BEFORE_METADATA_CAS";
pub const FAILPOINT_AFTER_METADATA_CAS: &str = "FAILPOINT_AFTER_METADATA_CAS";
pub const FAILPOINT_BEFORE_REPLY: &str = "FAILPOINT_BEFORE_REPLY";
pub const FAILPOINT_COMPACTION_AFTER_PUT: &str = "FAILPOINT_COMPACTION_AFTER_PUT";
pub const FAILPOINT_GC_BEFORE_DELETE: &str = "FAILPOINT_GC_BEFORE_DELETE";

pub fn armed(name: &str) -> bool;          // 两 env 都对
pub fn trip(name: &str);                   // armed 则 hook 或 abort
pub fn set_hook(hook: fn(&str));           // 测 / 可控崩溃
pub fn clear_hook();
pub fn seed() -> Option<u64>;              // 解析 SEED;harness 日志用

failpoint!(NAME);                          // → trip(NAME)
}

以后调用点长这样(现在不要提交这些行):

#![allow(unused)]
fn main() {
// chunk-plane/src/writer.rs  after upload_blocks
swift_failpoints::failpoint!(swift_failpoints::FAILPOINT_AFTER_BLOCK_PUT);

// swift-fuse/src/chunk_fs.rs  flush_handle, before commit_inode
swift_failpoints::failpoint!(swift_failpoints::FAILPOINT_BEFORE_METADATA_CAS);
}

生产:两 env 都不设 → 编译进二进制也是 no-op。不要 #[cfg(test)] 把宏拿掉:集群要测的是 同一份 release 二进制。


4. Harness 环(设计,本会话不跑)

合同:seed 1..=10000,每次 crash/restart 后 只有 orphan,从无 half-visible。 10000 是 ROADMAP 数字。本会话不跑;下面是以后在 swift1 上跑的环(Mac 不编、不 ssh 杀进程)。

4.1 一次 seed

seed S in 1..=N
site = SITES[(S - 1) % 6]          # 六种点均匀铺开
# 可选:SITES[(S-1)/6 % 6] 若只要某种点的连续段

1. 停干净:数字 PID 或 pkill -9 -x swiftfuse;umount -l;禁止 pkill -f
2. 起 daemon,环境:
     SWIFTFUSE_FAILPOINT=<site>
     SWIFTFUSE_FAILPOINT_SEED=<S>
   二进制仍是 /usr/local/bin/swiftfuse(先拷 /tmp/swiftfuse.new 再 install)
3. 探针进程(不是第二个 100k python):
     写唯一文件  /mnt/swift-etcd/r6-fp/s-<S>
     内容 = 已知 64KiB 图案(或 4K × 16)
     fsync
   预期:daemon 在选定点 abort。探针得到 EIO / 连接断。timeout --kill-after=2 15
4. 确认 swiftfuse 已死(pid 文件 / pgrep -x)。必要时 SIGKILL 数字 PID
5. **清掉两 env**,再起 daemon(否则一上来又 abort)
6. 判定(见 4.2)
7. 删探针文件(若可见)+ 记一行 JSONL

N=10000。先用 N=6 把六点各打一次,再 N=60,最后 N=10000。全量要占挂载:单独排期,不要和 G1/G6/100k 并行。

4.2 判定(half-visible 的定义)

对 /mnt/swift-etcd/r6-fp/s-<S>:

观察PASSFAIL
ENOENT允许(CAS 前死)。Swift 容器里可以有无 inode 引用的对象名在 readdir 里但 LOOKUP ENOENT
存在,size=0 且从未 fsync 成功允许size>0 但读到短于 size 的有效图案 / 洞里是旧垃圾当新数据
存在,size=期望,字节全对允许(CAS 后死)size=期望但 crc 错
存在,size 不是 0 也不是期望FAIL(半可见)—
etcd inodes/{id} 的 chunk 指针指向 404 的 Swift 对象FAIL—
compaction 点:读字节 ≠ 崩溃前 snapshotFAIL—

辅助(审计,不是 POSIX):枚举 {fs_name}-data 容器,减去 etcd 里所有 BlockRef.obj。差集 = orphan。orphan 计数只增不减 可以,不得 被任何活着的 inode 引用。

不要用 “exit code 0” 当 PASS。每行 JSONL 必须有 seed site visible size verdict。没有 verdict= 的汇总 = INCONCLUSIVE。

4.3 以后的脚本形状(现在不要写进 scripts/detect)

建议 scripts/r6-failpoint-loop.sh(未写):

N=${1:-10000}
for S in $(seq 1 $N); do
  site=...
  start_with_failpoint "$site" "$S"
  timeout --kill-after=2 15 python3 scripts/r6-failpoint-probe.py "$S" || true
  kill_daemon_numeric
  start_clean
  python3 scripts/r6-failpoint-judge.py "$S" >> "$OUT/results.jsonl"
done
# 汇总:grep FAIL;零 FAIL 且 rows==N → PASS

约束(HANDOFF §7):--kill-after;数字 PID;测完四节点 kernel_ttl=0;不 cargo clean;不 wf.sh find-touch;不并行第二只写挂载的 python。

4.4 和 G4b / 现有 G7c 的区别

G4b / G7cR6 failpoint
怎么死随机 SIGKILL / fio 中途杀指定 在 PUT/CAS/回复/compact/GC 切开
证明已 commit 的 crc 还在未 commit 的不可见;已 commit 的完整;指针不指向空洞
次数一次耐久10000 seeds,六点循环

G4b 过了 ≠ failpoint 合同关了。


5. G7 fencing 考试 — 只设计 harness(活门是 R2)

HANDOFF-2026-08-15:G6 lease 在 101550 能工作,不是 R2 门禁。 R2 门禁:partition → 旧 writer 租约过期 → 新 writer → 旧 writer 复活 commit 必须 100% 失败。

现有 G7a/b/c(etcd move-leader / netem / SIGKILL、已提交数据还在)是另一张成绩单。fencing 考试是 扩展,不要和 08-09 的 G7 PASS 混为一谈。

5.1 场景(实现 R2 的 token/epoch 之后再跑)

节点 W_old = swift2    持有 inode L 的写 lease,已 PUT 一批 block,尚未 CAS
          或:已 CAS 一版,又有第二批 dirty
节点 W_new = swift3
etcd      = 仍在 swift1(或活着的多数派)

1. W_old 打开 /r6-g7/fence.dat,写 32KiB 图案 A,fsync(commit v1)
2. W_old 再写 32KiB 图案 B,upload 完成,停在 CAS 前
   (R2 落地后可用 FAILPOINT_BEFORE_METADATA_CAS;或 iptables 丢掉 W_old→etcd)
3. 分区:W_old 到 etcd 的 2379 丢掉(iptables OUTPUT / 输入)。不要杀 etcd
4. 等 lease TTL(现役 30s)+ 2s
5. W_new 打开同一文件,写图案 C,fsync。必须成功(新 fencing_epoch)
6. 愈合 W_old
7. W_old 的未完成 flush 继续 commit_inode / 旧 lease id
8. 断言:该 commit **100%** Conflict 或 fencing 拒绝。etcd 上的 version/epoch 仍是 W_new 的
9. 第三客户(swift4)读:看见 A+C(或 R2 规定的可见前缀),**不许** 看见 B 盖掉 C

5.2 判定

检查必须
W_old 复活 CAS失败。一次成功 = FAIL(fencing 没挡住)
W_new fsync成功
第三客户字节等于 W_new 的 commit,不是 W_old 的迟到切片
lease keyholder / epoch 是 W_new,不是愈合后的 W_old

跑 100 次分区环(不是 10000;10000 是 failpoint 数据面)。100/100 旧 commit 失败才写 G7-FENCE-VERDICT: PASS。

5.3 本轮不做

  • 不写 iptables 脚本,不分区,不改 lease 格式。
  • 不把 fencing 考试当 R6 crate 的单测。crate 只证明 env trip。
  • R2 agent 拥有 etcd.rs 的 epoch / client_id。harness 挂在 R2 的 token 上,不抢。

6. 非目标

  • 不重做 Porcupine fixture / porcupine_check.sh。
  • 不把 failpoint 做成编译 feature flag(集群要同一条 release 线)。
  • 不在 Mac 上 cargo test 当集群证据。脚手架单测是逻辑证明;10000 是集群合同。
  • 不 pkill -f、不 cargo clean、不 wf.sh、不本轮 remount。

7. 以后的接入顺序

  1. 根 Cargo.toml 加 member;chunk-plane + swift-fuse 加 path dep。
  2. 六个 failpoint! 按 §2 表插入。一次一个文件的 PR 面,避免和 R2 同改 etcd.rs。
  3. N=6 烟雾(每点一次)在 swift1,独占挂载。
  4. N=10000 排期。
  5. G7 fencing 等 R2 token 合并后再写 orchestrator。