crash_recovery_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package core
  2. import (
  3. "os"
  4. "os/exec"
  5. "path/filepath"
  6. "testing"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  9. )
  10. // TestCrashRecoveryCommittedOnly 崩溃恢复边界: 子进程提交第一批后, 写入
  11. // 第二批【不做任何提交】并以 os.Exit 立即死亡 (模拟断电/被杀, 不走红色的
  12. // Close 兜底提交)。父进程重开数据库验证:
  13. // - 已提交 (WAL 已落盘) 的第一批必须完整可见 —— 崩溃恢复的职责;
  14. // - 未提交的第二批必须完全不可见 —— 提交边界的职责;
  15. // - 文件可正常打开、无结构损坏。
  16. //
  17. // 子进程通过环境变量自识别 (go test 子进程测试的标准模式)。
  18. func TestCrashRecoveryCommittedOnly(t *testing.T) {
  19. dir := t.TempDir()
  20. path := filepath.Join(dir, "crash.db")
  21. // 子进程: 提交 500 行 → 再写 500 行不提交 → 暴毙
  22. childEnv := append(os.Environ(),
  23. "TSDB_CRASH_CHILD=1",
  24. "TSDB_CRASH_DB="+path,
  25. )
  26. cmd := exec.Command(os.Args[0], "-test.run=TestCrashChildMain", "-test.v")
  27. cmd.Env = childEnv
  28. out, err := cmd.CombinedOutput()
  29. if err != nil {
  30. t.Fatalf("子进程: %v\n%s", err, out)
  31. }
  32. // 父进程: 重开 (WAL 恢复发生在这里)
  33. db, err := Open(path)
  34. if err != nil {
  35. t.Fatalf("崩溃后重开失败: %v", err)
  36. }
  37. defer db.Close()
  38. tbl, ok := db.FindTable("t")
  39. if !ok {
  40. t.Fatalf("表丢失")
  41. }
  42. if c := vacCountOf(t, tbl); c != 500 {
  43. t.Fatalf("崩溃后可见行数 = %d, 想要 500 (已提交批次); 未提交批次必须不可见", c)
  44. }
  45. // 已提交行的值正确
  46. for _, id := range []uint64{1, 250, 500} {
  47. vals, err := tbl.FindByPK(id)
  48. if err != nil || !hasColVal(vals, "v") {
  49. t.Fatalf("已提交行 id=%d 读取失败: %v %+v", id, err, vals)
  50. }
  51. }
  52. // 未提交行必须不存在
  53. for _, id := range []uint64{501, 750, 1000} {
  54. if _, err := tbl.FindByPK(id); err == nil {
  55. t.Fatalf("未提交行 id=%d 在崩溃后可见 (提交边界被破坏)", id)
  56. }
  57. }
  58. // 写入能力未损坏: 新行可插入可回读
  59. if _, err := tbl.InsertRowWithID(1001, []types.FieldValue{
  60. {FieldName: []byte("id"), Value: uint64(1001)},
  61. {FieldName: []byte("v"), Value: int64(1)},
  62. }); err != nil {
  63. t.Fatalf("崩溃后写入失败: %v", err)
  64. }
  65. if err := db.Commit(); err != nil {
  66. t.Fatalf("commit: %v", err)
  67. }
  68. if c := vacCountOf(t, tbl); c != 501 {
  69. t.Fatalf("写入后行数 = %d, 想要 501", c)
  70. }
  71. }
  72. // TestCrashRecoveryCommittedOnly 的子进程主体 (环境变量 TSDB_CRASH_CHILD=1 时)。
  73. // 注意: 这个"测试"在子进程里不会跑到 t 的断言, 它只做写入然后 os.Exit。
  74. func TestCrashChildMain(t *testing.T) {
  75. if os.Getenv("TSDB_CRASH_CHILD") != "1" {
  76. return // 正常测试运行: 什么都不做
  77. }
  78. path := os.Getenv("TSDB_CRASH_DB")
  79. db, err := Open(path)
  80. if err != nil {
  81. os.Exit(2)
  82. }
  83. db.SetAutoCommitDelay(0) // 关掉防抖: 提交完全由显式 Commit 控制
  84. tbl, err := db.CreateTable("t", []types.FieldInfo{
  85. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  86. {FieldType: types.FieldType_Int64, FieldName: []byte("v")},
  87. })
  88. if err != nil {
  89. os.Exit(2)
  90. }
  91. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  92. os.Exit(2)
  93. }
  94. // 第一批: 显式提交 (WAL + fsync, 必须扛住崩溃)
  95. for i := 1; i <= 500; i++ {
  96. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  97. {FieldName: []byte("id"), Value: uint64(i)},
  98. {FieldName: []byte("v"), Value: int64(i)},
  99. }); err != nil {
  100. os.Exit(2)
  101. }
  102. }
  103. if err := db.Commit(); err != nil {
  104. os.Exit(2)
  105. }
  106. // 第二批: 只进 BufferPool, 永不提交
  107. for i := 501; i <= 1000; i++ {
  108. if _, err := tbl.InsertRowWithID(uint64(i), []types.FieldValue{
  109. {FieldName: []byte("id"), Value: uint64(i)},
  110. {FieldName: []byte("v"), Value: int64(i)},
  111. }); err != nil {
  112. os.Exit(2)
  113. }
  114. }
  115. // 故意不调 Close / Commit: os.Exit 让脏页随进程蒸发 (模拟断电)
  116. os.Exit(0)
  117. }