wal_page_rest_test.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package core
  2. // =============================================================================
  3. // WALOpPageRest 补尾记录回归测试
  4. //
  5. // 背景 (demo/wal_crash_chaos 随机断电压测实锤): 整页镜像按 useSize 截断存 WAL,
  6. // 而 DataPage 的槽位目录在页尾 (useSize 之外) —— 崩溃恢复零填充把目录抹掉,
  7. // 页上全部行经槽位不可达 (TotalRows 与实际可达行数分叉)。Heap/Blob 满页同理
  8. // 会被"单条容量上限"截掉真实数据。修复 = FullPage + PageRest 成对入链, 恢复端
  9. // 按显式偏移覆盖页尾。
  10. // =============================================================================
  11. import (
  12. "bytes"
  13. "encoding/binary"
  14. "testing"
  15. "git.ycl.pub/yl32940/TsDB2/DB/core/index/datapage"
  16. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  17. )
  18. // stubHandleDataPage 内存态数据页: NewPage 自增分配, SavePage 存回 map。
  19. // 用来造一张带真实槽位目录的 DataPage 镜像, 喂给 WALWriter.packEntries。
  20. func stubHandleDataPage(t *testing.T, rows int) (full []byte, useSize, dirStart uint32) {
  21. t.Helper()
  22. pages := map[uint32][]byte{}
  23. next := uint32(1000)
  24. h := page.HandleDb{
  25. NewPage: func() uint32 { p := next; next++; return p },
  26. SavePage: func(pid uint32, buf []byte) error {
  27. b := make([]byte, len(buf))
  28. copy(b, buf)
  29. pages[pid] = b
  30. return nil
  31. },
  32. ReadPage: func(pid uint32, buf []byte) error {
  33. copy(buf, pages[pid])
  34. return nil
  35. },
  36. }
  37. dp := datapage.New(h)
  38. for i := 0; i < rows; i++ {
  39. row := make([]byte, 160) // 行: BE 键 + 填充
  40. binary.BigEndian.PutUint64(row[:8], uint64(295+i))
  41. if dp.AppendRowSlot(row) == 0 {
  42. t.Fatalf("AppendRowSlot 第 %d 行失败", i)
  43. }
  44. }
  45. dp.Save()
  46. full = pages[dp.PageID()]
  47. useSize = binary.LittleEndian.Uint32(full[16:])
  48. ds, ok := datapage.SlotDirStart(full)
  49. if !ok {
  50. t.Fatalf("造页失败: 槽位目录无效")
  51. }
  52. return full, useSize, ds
  53. }
  54. // TestPackEntriesDataPageTail 截断的 DataPage 必须展开成 FullPage+PageRest,
  55. // 且补尾的显式偏移 = 槽位目录起始, 内容 = 目录区原文。
  56. func TestPackEntriesDataPageTail(t *testing.T) {
  57. full, useSize, ds := stubHandleDataPage(t, 21)
  58. if ds <= useSize {
  59. t.Fatalf("前置条件: 目录应在 useSize 之外 (useSize=%d ds=%d)", useSize, ds)
  60. }
  61. w := &WALWriter{pageIndex: map[uint32]int{}}
  62. w.RecordPage(9, full)
  63. packed := w.packEntries()
  64. if len(packed) != 2 {
  65. t.Fatalf("应展开成 2 条 (FullPage+PageRest), 实际 %d", len(packed))
  66. }
  67. if packed[0].opType != WALOpFullPage || uint32(len(packed[0].payload)) != useSize {
  68. t.Fatalf("base: op=%d len=%d, 想要 FullPage/%d", packed[0].opType, len(packed[0].payload), useSize)
  69. }
  70. if !bytes.Equal(packed[0].payload, full[:useSize]) {
  71. t.Fatalf("base 内容 ≠ 页有效前缀")
  72. }
  73. rest := packed[1]
  74. if rest.opType != WALOpPageRest || rest.pageID != 9 {
  75. t.Fatalf("补尾: op=%d page=%d, 想要 PageRest/9", rest.opType, rest.pageID)
  76. }
  77. off := binary.LittleEndian.Uint32(rest.payload[:4])
  78. if off != ds {
  79. t.Fatalf("补尾偏移=%d, 想要目录起始 %d", off, ds)
  80. }
  81. if !bytes.Equal(rest.payload[4:], full[ds:]) {
  82. t.Fatalf("补尾内容 ≠ 目录区原文")
  83. }
  84. }
  85. // TestPackEntriesCapAndSkips 容量上限截掉的尾数据必须补 (Heap 满页);
  86. // 未被截断的非数据页 (B+ 树节点) 与头页不产生补尾。
  87. func TestPackEntriesCapAndSkips(t *testing.T) {
  88. // Heap 满页: useSize=16384 > 单条上限 → base 截到 16331, 补 [16331, 16384)
  89. heap := make([]byte, PageSize)
  90. page.HdrOf(heap).Type = page.HeapIndex
  91. page.HdrOf(heap).UseSize = PageSize
  92. w := &WALWriter{pageIndex: map[uint32]int{}}
  93. w.RecordPage(8, heap)
  94. packed := w.packEntries()
  95. if len(packed) != 2 {
  96. t.Fatalf("Heap 满页应产生补尾, 实际 %d 条", len(packed))
  97. }
  98. if uint32(len(packed[0].payload)) != PageSize-page.WALPageHdrSize-page.WALEntryMeta {
  99. t.Fatalf("base 应截到容量上限, 实际 %d", len(packed[0].payload))
  100. }
  101. if off := binary.LittleEndian.Uint32(packed[1].payload[:4]); off != uint32(len(packed[0].payload)) {
  102. t.Fatalf("补尾偏移=%d, 想要 %d", off, len(packed[0].payload))
  103. }
  104. if uint32(len(packed[1].payload)) != 4+(PageSize-16331) {
  105. t.Fatalf("补尾长度=%d, 想要 %d", len(packed[1].payload), 4+(PageSize-16331))
  106. }
  107. // B+ 树节点 (useSize 远小于上限): useSize 内完整入链, 无补尾
  108. bt := make([]byte, PageSize)
  109. page.HdrOf(bt).Type = page.IndexDataIndex
  110. page.HdrOf(bt).UseSize = 4442
  111. w2 := &WALWriter{pageIndex: map[uint32]int{}}
  112. w2.RecordPage(7, bt)
  113. if p2 := w2.packEntries(); len(p2) != 1 {
  114. t.Fatalf("B+ 节点不应产生补尾, 实际 %d 条", len(p2))
  115. }
  116. // 头页镜像 (4KB): 无补尾
  117. hdr := make([]byte, PageSize4K)
  118. w3 := &WALWriter{pageIndex: map[uint32]int{}}
  119. w3.RecordPage(0, hdr)
  120. if p3 := w3.packEntries(); len(p3) != 1 {
  121. t.Fatalf("头页镜像不应产生补尾, 实际 %d 条", len(p3))
  122. }
  123. }
  124. // TestRecoverWALPageRestRoundTrip 恢复端: FullPage+PageRest 重放后, 整页字节
  125. // (含 useSize 之外的槽位目录) 必须与原始镜像逐字节一致 —— 这是压测抓到的
  126. // 丢数据路径的直接回归。
  127. func TestRecoverWALPageRestRoundTrip(t *testing.T) {
  128. path, ff := walHardeningDB(t)
  129. walPID := nextFreePage(t, ff)
  130. targetPID := walPID + 5 // 不与 WAL 首页相撞
  131. full, useSize, ds := stubHandleDataPage(t, 21)
  132. base := encodeWALEntry(targetPID, WALOpFullPage, full[:useSize], 0)
  133. restPayload := make([]byte, 4+PageSize-ds)
  134. binary.LittleEndian.PutUint32(restPayload, ds)
  135. copy(restPayload[4:], full[ds:])
  136. rest := encodeWALEntry(targetPID, WALOpPageRest, restPayload, 0)
  137. first := buildWALFirstPage(t, walPID, 0, 2, base, rest)
  138. publishWALChain(t, ff, first)
  139. _ = ff.Close()
  140. db, err := Open(path)
  141. if err != nil {
  142. t.Fatalf("重开 (重放 FullPage+PageRest): %v", err)
  143. }
  144. _ = db.Close()
  145. // 直读目标页, 与原始镜像逐字节比对 (重点是 useSize 之外的目录区)
  146. ff2, err := OpenFile(path)
  147. if err != nil {
  148. t.Fatalf("重开裸文件: %v", err)
  149. }
  150. defer ff2.Close()
  151. got := make([]byte, PageSize)
  152. if err := ff2.readPageDisk(targetPID, got); err != nil {
  153. t.Fatalf("读目标页: %v", err)
  154. }
  155. if !bytes.Equal(got, full) {
  156. t.Fatalf("重放后页 %d 与原始镜像不一致 (useSize=%d, 目录起始=%d)", targetPID, useSize, ds)
  157. }
  158. }
  159. // TestRecoverWALOrphanPageRestRejected 孤儿补尾 (没有同页前置整页镜像) 的已提交链
  160. // 必须拒绝重放并保留 WAL —— 补尾只有叠在 base 写回的页上才有意义, 单独出现
  161. // 说明链损坏/被篡改, 部分重放会把零填充页当真实数据。
  162. func TestRecoverWALOrphanPageRestRejected(t *testing.T) {
  163. path, ff := walHardeningDB(t)
  164. walPID := nextFreePage(t, ff)
  165. restPayload := make([]byte, 8)
  166. binary.LittleEndian.PutUint32(restPayload, 100)
  167. rest := encodeWALEntry(5, WALOpPageRest, restPayload, 0)
  168. first := buildWALFirstPage(t, walPID, 0, 1, rest)
  169. publishWALChain(t, ff, first)
  170. _ = ff.Close()
  171. db, err := Open(path)
  172. if err == nil {
  173. _ = db.Close()
  174. t.Fatalf("孤儿补尾的已提交链必须让 Open 返回错误, 却成功了")
  175. }
  176. t.Logf("正确拒绝: %v", err)
  177. ff2, err := OpenFile(path)
  178. if err != nil {
  179. t.Fatalf("重开裸文件: %v", err)
  180. }
  181. hdr, err := ff2.ReadHeader()
  182. _ = ff2.Close()
  183. if err != nil {
  184. t.Fatalf("读头页: %v", err)
  185. }
  186. if hdr.FirstWALPage != walPID {
  187. t.Fatalf("WAL 链应原样保留: FirstWALPage=%d, 想要 %d", hdr.FirstWALPage, walPID)
  188. }
  189. }