|
@@ -43,9 +43,20 @@ const (
|
|
|
envRounds = "TSDB_CRASH_ROUNDS"
|
|
envRounds = "TSDB_CRASH_ROUNDS"
|
|
|
envSeed = "TSDB_CRASH_SEED"
|
|
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() {
|
|
func main() {
|
|
@@ -53,6 +64,10 @@ func main() {
|
|
|
runChild()
|
|
runChild()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ if os.Getenv(envRecChd) == "1" {
|
|
|
|
|
+ runRecoverChild()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
runParent()
|
|
runParent()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -78,7 +93,7 @@ func checksumFor(id uint64) uint64 {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// =============================================================================
|
|
// =============================================================================
|
|
|
-// 子进程: 建库, 然后无限循环 "插 7 行 → Commit", 直到被强杀
|
|
|
|
|
|
|
+// 子进程: 建库, 然后无限循环 "插 batchSize 行 → Commit", 直到被强杀
|
|
|
// =============================================================================
|
|
// =============================================================================
|
|
|
|
|
|
|
|
func runChild() {
|
|
func runChild() {
|
|
@@ -109,12 +124,12 @@ func runChild() {
|
|
|
}); err != nil {
|
|
}); err != nil {
|
|
|
os.Exit(2)
|
|
os.Exit(2)
|
|
|
}
|
|
}
|
|
|
- if i%batchSize == 0 {
|
|
|
|
|
|
|
+ if i%uint64(batchSize) == 0 {
|
|
|
if err := db.Commit(); err != nil { // WAL 写链 + fsync ×2 发生在这里
|
|
if err := db.Commit(); err != nil { // WAL 写链 + fsync ×2 发生在这里
|
|
|
os.Exit(2)
|
|
os.Exit(2)
|
|
|
}
|
|
}
|
|
|
// 干净模式: 写满 N 批后正常关闭 (校验压测装置本身: 走 Close 路径必须零损伤)
|
|
// 干净模式: 写满 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 {
|
|
if err := db.Close(); err != nil {
|
|
|
os.Exit(2)
|
|
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
|
|
return err
|
|
|
}
|
|
}
|
|
|
cmd := exec.Command(exe)
|
|
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 {
|
|
if cleanBatches := envInt("TSDB_CRASH_CLEAN_BATCHES", 0); cleanBatches > 0 {
|
|
|
// 干净模式: 子进程写满 N 批后自行 Close 退出, 不杀 —— 校验压测装置本身
|
|
// 干净模式: 子进程写满 N 批后自行 Close 退出, 不杀 —— 校验压测装置本身
|
|
|
cmd.Env = append(cmd.Env, "TSDB_CRASH_CLEAN_BATCHES="+strconv.Itoa(cleanBatches))
|
|
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)
|
|
return fmt.Errorf("干净模式子进程异常退出: %v", err)
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} 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.Process.Kill()
|
|
|
_ = cmd.Wait() // 回收句柄, 确保文件锁释放后再重开
|
|
_ = 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) {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
|
os.RemoveAll(dir)
|
|
os.RemoveAll(dir)
|
|
|
return nil // 子进程在建文件前被杀, 本轮无对象可验
|
|
return nil // 子进程在建文件前被杀, 本轮无对象可验
|
|
@@ -198,6 +241,22 @@ func oneRound(rng *rand.Rand) error {
|
|
|
return os.RemoveAll(dir)
|
|
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 {
|
|
func copyFile(src, dst string) error {
|
|
|
in, err := os.Open(src)
|
|
in, err := os.Open(src)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -243,7 +302,12 @@ func verify(path, dir string) error {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 提交边界: K 之后必须完全不可见 (半截事务 + 在途行)
|
|
// 提交边界: 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 {
|
|
if _, err := tbl.FindByPK(id); err == nil {
|
|
|
return fmt.Errorf("未提交行 id=%d 在恢复后可见, 提交边界被破坏 (现场: %s)", id, dir)
|
|
return fmt.Errorf("未提交行 id=%d 在恢复后可见, 提交边界被破坏 (现场: %s)", id, dir)
|
|
|
}
|
|
}
|