release_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package btree
  2. import (
  3. "encoding/binary"
  4. "sort"
  5. "testing"
  6. "TsDB2/DB/core/page"
  7. )
  8. // ============================================================================
  9. // 页回收回归测试 —— 钉死「二级 B+ 树整棵树必须被完整归还」。
  10. //
  11. // 背景 (bench_10m 审计发现的真实缺陷):
  12. // CollectAllPageIDs 下探内部节点时一律用 btPID(e) 读「偏移 8..12」的子页号。
  13. // 这只对主键条目成立 ([key(8B)][childPID(4B)])。二级 B+ 树的内部条目是变长的
  14. // ([key(keyLen)][childPID(4B)], entryLen = keyLen+4 或 keyLen+8), 子页号在**末 4 字节**。
  15. // 于是 BFS 在二级树上读到的永远是 0 (或键值字节, 一个从未分配过的垃圾页号):
  16. // 整棵子树只回收了根页, 其余全部成为永久孤儿页 —— 文件只涨不缩。
  17. //
  18. // 实测: 千万行库里 DROP INDEX / 批量删除重建 / DROP TABLE 之后残留 32444 个孤儿
  19. // 索引页 (约 3.2 万页 × 16KB ≈ 500MB), 全部来自二级树子树。
  20. //
  21. // 这些用例用「按键分配的内存页句柄」直接观测「分配了多少页 / 归还了多少页」,
  22. // 不依赖 CollectAllPageIDs 自身去数页 —— 避免循环论证 (若遍历与回收都漏收同一批页,
  23. // 互相印证反而会「通过」)。
  24. // ============================================================================
  25. // memHandle 一个只在内存里保存 16KB 页的 HandleDb:
  26. // 记录每个分配出去的页号与每次归还的页号, 供断言直接比对。
  27. type memHandle struct {
  28. pages map[uint32][]byte
  29. next uint32 // 最近一次分配出去的页号 (0/1 预留, 首个可分配页是 2)
  30. freed []uint32
  31. }
  32. func newMemHandle() *memHandle {
  33. return &memHandle{pages: map[uint32][]byte{}, next: 1}
  34. }
  35. func (m *memHandle) handle() page.HandleDb {
  36. return page.HandleDb{
  37. NewPage: func() uint32 {
  38. m.next++
  39. m.pages[m.next] = make([]byte, page.IndexPageSize)
  40. return m.next
  41. },
  42. SavePage: func(pid uint32, buf []byte) error {
  43. dst := make([]byte, page.IndexPageSize)
  44. copy(dst, buf)
  45. m.pages[pid] = dst
  46. return nil
  47. },
  48. ReadPage: func(pid uint32, buf []byte) error {
  49. src, ok := m.pages[pid]
  50. if !ok {
  51. return page.ErrNotFound
  52. }
  53. copy(buf, src)
  54. return nil
  55. },
  56. // 直接归还与延迟归还都记账, 用例只关心「有没有被登记回收」。
  57. FreePage: func(pid uint32) { m.freed = append(m.freed, pid) },
  58. DeferFreePage: func(pid uint32) { m.freed = append(m.freed, pid) },
  59. // 与引擎一致: 0 是头页, 1 是建库预留表页, >= next 的页从未分配过。
  60. ValidPage: func(pid uint32) bool { return pid >= 2 && pid < m.next },
  61. }
  62. }
  63. // allocated 返回全部已分配页号 (升序)。
  64. func (m *memHandle) allocated() []uint32 {
  65. out := make([]uint32, 0, len(m.pages))
  66. for pid := range m.pages {
  67. out = append(out, pid)
  68. }
  69. sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
  70. return out
  71. }
  72. func sortedSet(ids []uint32) []uint32 {
  73. seen := map[uint32]bool{}
  74. out := make([]uint32, 0, len(ids))
  75. for _, id := range ids {
  76. if !seen[id] {
  77. seen[id] = true
  78. out = append(out, id)
  79. }
  80. }
  81. sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
  82. return out
  83. }
  84. func sameSet(a, b []uint32) bool {
  85. if len(a) != len(b) {
  86. return false
  87. }
  88. for i := range a {
  89. if a[i] != b[i] {
  90. return false
  91. }
  92. }
  93. return true
  94. }
  95. // assertReclaimsWholeTree 构建一棵树后断言: CollectAllPageIDs 枚举到的页
  96. // == 实际分配出去的全部页 (即整棵树), 且 btReleaseSubtree 把它们全部登记回收。
  97. func assertReclaimsWholeTree(t *testing.T, m *memHandle, root uint32) {
  98. t.Helper()
  99. allocated := m.allocated()
  100. if len(allocated) <= 1 {
  101. t.Fatalf("样本太小: 只分配了 %d 页, 覆盖不到「多级树」; 请增大条目数", len(allocated))
  102. }
  103. got := sortedSet(CollectAllPageIDs(m.handle(), root))
  104. if !sameSet(got, allocated) {
  105. t.Fatalf("CollectAllPageIDs 漏收页: 分配到 %d 页 %v, 只枚举出 %d 页 %v\n"+
  106. "内部节点下探用了错误的子页号偏移 (二级树子页号在末 4 字节, 不在偏移 8..12)",
  107. len(allocated), allocated, len(got), got)
  108. }
  109. m.freed = nil
  110. btReleaseSubtree(m.handle(), root)
  111. if freed := sortedSet(m.freed); !sameSet(freed, allocated) {
  112. t.Fatalf("btReleaseSubtree 漏归还: 应归还 %d 页 %v, 实际归还 %d 页 %v",
  113. len(allocated), allocated, len(freed), freed)
  114. }
  115. }
  116. // TestCollectAllPageIDsSecondaryTree 二级 B+ 树 (IndexTypeBTree, 内部条目子页号在末 4 字节)。
  117. //
  118. // 键取 16 字节且第 2 个 8 字节非零, 正是为了把旧实现的读法暴露出来:
  119. // 偏移 8..12 落在键值上 —— 旧的 btPID 会截出一个巨大的、从未分配过的「页号」。
  120. // 这类垃圾页号若不加 ValidPage 兜底就会直接写进空闲链, 那比泄漏更严重 (静默数据损坏)。
  121. func TestCollectAllPageIDsSecondaryTree(t *testing.T) {
  122. const (
  123. keyLen = 16
  124. rows = 4000 // 二级叶容量 ≈ 681 条 (entryLen=24) → 6 叶 + 1 根
  125. )
  126. m := newMemHandle()
  127. entries := make([][]byte, rows)
  128. for i := 0; i < rows; i++ {
  129. e := make([]byte, keyLen+8)
  130. binary.BigEndian.PutUint64(e[0:8], uint64(i)) // 键分量 1
  131. binary.BigEndian.PutUint64(e[8:16], uint64(i)*7919+1) // 键分量 2 (非零, 旧实现会误读)
  132. binary.BigEndian.PutUint64(e[keyLen:], uint64(i)+1) // PKID
  133. entries[i] = e
  134. }
  135. root := SBTBuildFromSorted(entries, m.handle())
  136. if root == 0 {
  137. t.Fatal("SBTBuildFromSorted 返回根页 0")
  138. }
  139. assertReclaimsWholeTree(t, m, root)
  140. }
  141. // TestCollectAllPageIDsPrimaryTree 主键 B+ 树 (IndexTypePrimary, 子页号在偏移 8..12)。
  142. // 这条路径本来就对, 一并钉住 —— 修复分流后不能把主键树改坏。
  143. func TestCollectAllPageIDsPrimaryTree(t *testing.T) {
  144. const rows = 4000 // 主键叶容量 ≈ 1168 条 (entryLen=14) → 4 叶 + 1 根
  145. m := newMemHandle()
  146. entries := make([][]byte, rows)
  147. for i := 0; i < rows; i++ {
  148. e := make([]byte, btLeafEntryLen)
  149. binary.BigEndian.PutUint64(e[0:8], uint64(i))
  150. btSetPID(e, 12) // dpID (回收路径不看叶条目的 dpID, 只要合法即可)
  151. btSetSlot(e, uint16(1)) // slot
  152. entries[i] = e
  153. }
  154. root := BuildFromSorted(entries, m.handle())
  155. if root == 0 {
  156. t.Fatal("BuildFromSorted 返回根页 0")
  157. }
  158. assertReclaimsWholeTree(t, m, root)
  159. }