wal_meta_refresh_test.go 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. package core
  2. import (
  3. "encoding/binary"
  4. "testing"
  5. "unsafe"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  7. )
  8. // TestRecoverWALRefreshesTableMeta 回归: Open 必须先做 WAL 恢复、再载入表元数据。
  9. //
  10. // 背景 (wal_crash_chaos 压测实锤): 旧顺序 tablePage.Init → RecoverWAL 会让内存
  11. // Table 对象持有【恢复前】的表信息页 (TotalRows/堆链指针/RowSize), 恢复重放的
  12. // 新元数据只到磁盘 —— 内存与磁盘分叉: 行数统计错误, 堆链指针过期后后续写入
  13. // 按过期结构分配 = 损坏。
  14. func TestRecoverWALRefreshesTableMeta(t *testing.T) {
  15. // 建库: 一张表, 不提交任何行 (TotalRows=0)
  16. path := t.TempDir() + "/meta_refresh.db"
  17. db, err := Open(path)
  18. if err != nil {
  19. t.Fatalf("建库: %v", err)
  20. }
  21. db.SetAutoCommitDelay(0)
  22. tbl, err := db.CreateTable("t", []types.FieldInfo{
  23. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  24. {FieldType: types.FieldType_Uint64, FieldName: []byte("c")},
  25. {FieldType: types.FieldType_String, FieldName: []byte("p")},
  26. })
  27. if err != nil {
  28. t.Fatalf("建表: %v", err)
  29. }
  30. if _, err := tbl.CreateIndex("pk", 2, []byte{0}); err != nil {
  31. t.Fatalf("建索引: %v", err)
  32. }
  33. tablePID := tbl.TablePageID()
  34. startOff := tbl.TableStartOffset() // 表信息结构在表页内的偏移 (页头之后)
  35. if err := db.Close(); err != nil {
  36. t.Fatalf("关闭: %v", err)
  37. }
  38. // 伪造一条已提交 WAL: 把表信息页的 TotalRows 改成 4242 后整页镜像入链
  39. ff, err := OpenFile(path)
  40. if err != nil {
  41. t.Fatalf("裸开: %v", err)
  42. }
  43. walPID := nextFreePage(t, ff)
  44. pageBuf := make([]byte, PageSize)
  45. if err := ff.readPageDisk(tablePID, pageBuf); err != nil {
  46. t.Fatalf("读表页: %v", err)
  47. }
  48. // TotalRows 页内偏移 = 表信息结构起始偏移 + 结构内偏移 (与载入路径同口径)
  49. trOff := startOff + uint32(unsafe.Offsetof(TableInfo{}.TotalRows))
  50. before := binary.LittleEndian.Uint64(pageBuf[trOff:])
  51. binary.LittleEndian.PutUint64(pageBuf[trOff:], 4242)
  52. // 与真实写端一致: 整页镜像只存有效前缀 (表页 useSize 远小于页容量)
  53. useSize := binary.LittleEndian.Uint32(pageBuf[16:])
  54. entry := encodeWALEntry(tablePID, WALOpFullPage, pageBuf[:useSize], 0)
  55. first := buildWALFirstPage(t, walPID, 0, 1, entry)
  56. publishWALChain(t, ff, first)
  57. _ = ff.Close()
  58. // 重开: 恢复必须先于表元数据载入, 引擎读到的 TotalRows 必须是恢复后的 4242
  59. db2, err := Open(path)
  60. if err != nil {
  61. t.Fatalf("重开: %v", err)
  62. }
  63. defer db2.Close()
  64. tbl2, ok := db2.FindTable("t")
  65. if !ok {
  66. t.Fatal("表丢失")
  67. }
  68. if got := tbl2.TotalRows(); got != 4242 {
  69. t.Fatalf("恢复后的表元数据必须对引擎可见: TotalRows=%d, 想要 4242 (恢复前=%d)", got, before)
  70. }
  71. }