Quellcode durchsuchen

test(demo): wal_crash_chaos 支持重型写入与嵌套断电场景

- TSDB_CRASH_PAYLOAD / TSDB_CRASH_BATCH / TSDB_CRASH_WINDOW: 行负载与批大小可调。
  2KB 行 × 30 行/批时每个提交脏 ~10 页, WAL 链跨多页, 杀点更容易落进链中间
  (定长 string 列宽上限 2048B, 超出请改用动态列)。
- TSDB_CRASH_RECOVER_KILL=1: 杀完写进程后, 再起恢复进程打开数据库 (触发
  RecoverWAL 重放) 并在重放途中 0~30ms 随机时刻再次强杀 —— 验证'恢复被中断
  后重开'的幂等/原子性; 最终一致性仍由 verify 的四维断言兜底。
- 提交边界探测跨度随 batch 自适应 (半截事务可能更长)。
yl32940 vor 3 Wochen
Ursprung
Commit
dbe84441d8
1 geänderte Dateien mit 74 neuen und 10 gelöschten Zeilen
  1. 74 10
      demo/wal_crash_chaos/main.go

+ 74 - 10
demo/wal_crash_chaos/main.go

@@ -43,9 +43,20 @@ const (
 	envRounds = "TSDB_CRASH_ROUNDS"
 	envSeed   = "TSDB_CRASH_SEED"
 
-	payloadLen = 128
-	batchSize  = 7 // 每个显式事务提交的行数; K(已提交前缀) 必须是它的整数倍
-	probeExtra = 8 // 已提交前缀之后必须不存在的探测行数 (>= 半截事务最大 6 行 + 余量)
+	envPayload = "TSDB_CRASH_PAYLOAD" // 单行负载字节 (默认 128; 调大后每页行数变少)
+	envBatch   = "TSDB_CRASH_BATCH"   // 每个显式事务的行数 (默认 7)
+	envRecKill = "TSDB_CRASH_RECOVER_KILL"
+	envWindow  = "TSDB_CRASH_WINDOW" // 写进程随机存活上限 ms (默认 250)
+	envRecChd  = "TSDB_CRASH_RECOVER_CHILD"
+
+	probeExtra = 8 // 已提交前缀之后必须不存在的探测行数 (>= 半截事务最大行数 + 余量)
+)
+
+// 运行参数: 父进程通过 env 传给子进程, 保证校验端按同一参数重生成负载
+var (
+	payloadLen = envInt(envPayload, 128)
+	batchSize  = envInt(envBatch, 7)
+	killWindow = envInt(envWindow, 250)
 )
 
 func main() {
@@ -53,6 +64,10 @@ func main() {
 		runChild()
 		return
 	}
+	if os.Getenv(envRecChd) == "1" {
+		runRecoverChild()
+		return
+	}
 	runParent()
 }
 
@@ -78,7 +93,7 @@ func checksumFor(id uint64) uint64 {
 }
 
 // =============================================================================
-// 子进程: 建库, 然后无限循环 "插 7 行 → Commit", 直到被强杀
+// 子进程: 建库, 然后无限循环 "插 batchSize 行 → Commit", 直到被强杀
 // =============================================================================
 
 func runChild() {
@@ -109,12 +124,12 @@ func runChild() {
 		}); err != nil {
 			os.Exit(2)
 		}
-		if i%batchSize == 0 {
+		if i%uint64(batchSize) == 0 {
 			if err := db.Commit(); err != nil { // WAL 写链 + fsync ×2 发生在这里
 				os.Exit(2)
 			}
 			// 干净模式: 写满 N 批后正常关闭 (校验压测装置本身: 走 Close 路径必须零损伤)
-			if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 && int(i/batchSize) == cleanBatches {
+			if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 && int(i/uint64(batchSize)) == cleanBatches {
 				if err := db.Close(); err != nil {
 					os.Exit(2)
 				}
@@ -124,6 +139,24 @@ func runChild() {
 	}
 }
 
+// =============================================================================
+// 恢复进程 (嵌套断电): 打开数据库 —— RecoverWAL 重放在这里发生 —— 然后正常关闭。
+// 父进程会在打开后的随机时刻强杀本进程, 杀点落在重放途中:
+//   - 重放页写一半: 重开时头页仍指着 WAL 链, 整页镜像重放幂等, 必须无损;
+//   - 重放完成后 (头页已清 WAL 指针): 直接是一致状态。
+// 任何一条都会由父进程随后的 verify 兜底断言。
+// =============================================================================
+
+func runRecoverChild() {
+	path := os.Getenv(envDB)
+	db, err := core.Open(path)
+	if err != nil {
+		os.Exit(2) // 恢复报错 (如 CRC/断链拒绝) —— 父进程 Wait 会拿到非零退出
+	}
+	_ = db.Close()
+	os.Exit(0)
+}
+
 // =============================================================================
 // 父进程: 循环 起子进程 → 随机时刻强杀 → 重开校验
 // =============================================================================
@@ -161,7 +194,9 @@ func oneRound(rng *rand.Rand) error {
 		return err
 	}
 	cmd := exec.Command(exe)
-	cmd.Env = append(os.Environ(), envChild+"=1", envDB+"="+path)
+	cmd.Env = append(os.Environ(), envChild+"=1", envDB+"="+path,
+		envPayload+"="+strconv.Itoa(payloadLen),
+		envBatch+"="+strconv.Itoa(batchSize))
 	if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 {
 		// 干净模式: 子进程写满 N 批后自行 Close 退出, 不杀 —— 校验压测装置本身
 		cmd.Env = append(cmd.Env, "TSDB_CRASH_CLEAN_BATCHES="+strconv.Itoa(cleanBatches))
@@ -176,12 +211,20 @@ func oneRound(rng *rand.Rand) error {
 			return fmt.Errorf("干净模式子进程异常退出: %v", err)
 		}
 	} else {
-		// 随机存活 20~250ms: 杀点均匀覆盖 提交前 / WAL 写链中 / 页回写中 / 两 fsync 之间
-		time.Sleep(time.Duration(20+rng.Intn(230)) * time.Millisecond)
+		// 随机存活 20~killWindow ms: 杀点均匀覆盖 提交前 / WAL 写链中 / 页回写中 / 两 fsync 之间
+		time.Sleep(time.Duration(20+rng.Intn(killWindow-20)) * time.Millisecond)
 		_ = cmd.Process.Kill()
 		_ = cmd.Wait() // 回收句柄, 确保文件锁释放后再重开
 	}
 
+	// 嵌套断电: 再起恢复进程, 杀点落在 RecoverWAL 重放途中, 验证中断重开幂等
+	if os.Getenv(envRecKill) == "1" {
+		if err := killRecoverChild(exe, path, rng); err != nil {
+			os.RemoveAll(dir)
+			return err
+		}
+	}
+
 	if _, err := os.Stat(path); os.IsNotExist(err) {
 		os.RemoveAll(dir)
 		return nil // 子进程在建文件前被杀, 本轮无对象可验
@@ -198,6 +241,22 @@ func oneRound(rng *rand.Rand) error {
 	return os.RemoveAll(dir)
 }
 
+// killRecoverChild 起恢复进程并随机时刻强杀。重放耗时通常几十 ms (多次 fsync),
+// 0~30ms 的杀点大量落进重放途中。被杀的进程 Wait 必然返回 error (Windows 上是
+// exit status 1), 属于预期 —— 直接忽略; 恢复是否正确由随后 verify 的再次重开
+// 兜底断言 (verify 才是真相来源)。
+func killRecoverChild(exe, path string, rng *rand.Rand) error {
+	rcmd := exec.Command(exe)
+	rcmd.Env = append(os.Environ(), envRecChd+"=1", envDB+"="+path)
+	if err := rcmd.Start(); err != nil {
+		return err
+	}
+	time.Sleep(time.Duration(rng.Intn(30)) * time.Millisecond)
+	_ = rcmd.Process.Kill()
+	_ = rcmd.Wait()
+	return nil
+}
+
 func copyFile(src, dst string) error {
 	in, err := os.Open(src)
 	if err != nil {
@@ -243,7 +302,12 @@ func verify(path, dir string) error {
 	}
 
 	// 提交边界: K 之后必须完全不可见 (半截事务 + 在途行)
-	for id := k + 1; id <= k+probeExtra; id++ {
+	// 探测跨度取"半截事务最大行数 + 余量": batch 调大后半截事务可能更长
+	probe := probeExtra
+	if batchSize+1 > probe {
+		probe = batchSize + 1
+	}
+	for id := k + 1; id <= k+uint64(probe); id++ {
 		if _, err := tbl.FindByPK(id); err == nil {
 			return fmt.Errorf("未提交行 id=%d 在恢复后可见, 提交边界被破坏 (现场: %s)", id, dir)
 		}