package core import ( "os" "os/exec" "path/filepath" "testing" "git.ycl.pub/yl32940/TsDB2/DB/core/page" "git.ycl.pub/yl32940/TsDB2/DB/core/types" ) // TestCrashRecoveryCommittedOnly 崩溃恢复边界: 子进程提交第一批后, 写入 // 第二批【不做任何提交】并以 os.Exit 立即死亡 (模拟断电/被杀, 不走红色的 // Close 兜底提交)。父进程重开数据库验证: // - 已提交 (WAL 已落盘) 的第一批必须完整可见 —— 崩溃恢复的职责; // - 未提交的第二批必须完全不可见 —— 提交边界的职责; // - 文件可正常打开、无结构损坏。 // // 子进程通过环境变量自识别 (go test 子进程测试的标准模式)。 func TestCrashRecoveryCommittedOnly(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "crash.db") // 子进程: 提交 500 行 → 再写 500 行不提交 → 暴毙 childEnv := append(os.Environ(), "TSDB_CRASH_CHILD=1", "TSDB_CRASH_DB="+path, ) cmd := exec.Command(os.Args[0], "-test.run=TestCrashChildMain", "-test.v") cmd.Env = childEnv out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("子进程: %v\n%s", err, out) } // 父进程: 重开 (WAL 恢复发生在这里) db, err := Open(path) if err != nil { t.Fatalf("崩溃后重开失败: %v", err) } defer db.Close() tbl, ok := db.FindTable("t") if !ok { t.Fatalf("表丢失") } if c := vacCountOf(t, tbl); c != 500 { t.Fatalf("崩溃后可见行数 = %d, 想要 500 (已提交批次); 未提交批次必须不可见", c) } // 已提交行的值正确 for _, id := range []uint64{1, 250, 500} { vals, err := tbl.FindByPK(id) if err != nil || !hasColVal(vals, "v") { t.Fatalf("已提交行 id=%d 读取失败: %v %+v", id, err, vals) } } // 未提交行必须不存在 for _, id := range []uint64{501, 750, 1000} { if _, err := tbl.FindByPK(id); err == nil { t.Fatalf("未提交行 id=%d 在崩溃后可见 (提交边界被破坏)", id) } } // 写入能力未损坏: 新行可插入可回读 if _, err := tbl.InsertRowWithID(1001, []types.FieldValue{ {FieldName: []byte("id"), Value: uint64(1001)}, {FieldName: []byte("v"), Value: int64(1)}, }); err != nil { t.Fatalf("崩溃后写入失败: %v", err) } if err := db.Commit(); err != nil { t.Fatalf("commit: %v", err) } if c := vacCountOf(t, tbl); c != 501 { t.Fatalf("写入后行数 = %d, 想要 501", c) } } // TestCrashRecoveryCommittedOnly 的子进程主体 (环境变量 TSDB_CRASH_CHILD=1 时)。 // 注意: 这个"测试"在子进程里不会跑到 t 的断言, 它只做写入然后 os.Exit。 func TestCrashChildMain(t *testing.T) { if os.Getenv("TSDB_CRASH_CHILD") != "1" { return // 正常测试运行: 什么都不做 } path := os.Getenv("TSDB_CRASH_DB") db, err := Open(path) if err != nil { os.Exit(2) } db.SetAutoCommitDelay(0) // 关掉防抖: 提交完全由显式 Commit 控制 tbl, err := db.CreateTable("t", []types.FieldInfo{ {FieldType: types.FieldType_Uint64, FieldName: []byte("id")}, {FieldType: types.FieldType_Int64, FieldName: []byte("v")}, }) if err != nil { os.Exit(2) } if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil { os.Exit(2) } // 第一批: 显式提交 (WAL + fsync, 必须扛住崩溃) for i := 1; i <= 500; i++ { if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{ {FieldName: []byte("id"), Value: uint64(i)}, {FieldName: []byte("v"), Value: int64(i)}, }); err != nil { os.Exit(2) } } if err := db.Commit(); err != nil { os.Exit(2) } // 第二批: 只进 BufferPool, 永不提交 for i := 501; i <= 1000; i++ { if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{ {FieldName: []byte("id"), Value: uint64(i)}, {FieldName: []byte("v"), Value: int64(i)}, }); err != nil { os.Exit(2) } } // 故意不调 Close / Commit: os.Exit 让脏页随进程蒸发 (模拟断电) os.Exit(0) }