index_release_test.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. package core
  2. import (
  3. "testing"
  4. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  6. )
  7. // ============================================================================
  8. // 端到端页回收回归: DROP INDEX 必须把整棵二级索引树还给空闲链。
  9. //
  10. // 缺陷 (千万行审计发现): btree.CollectAllPageIDs 下探内部节点时统一按主键布局
  11. // 读「偏移 8..12」的子页号, 而二级 B+ 树的子页号在条目**末 4 字节**。结果 BFS 在
  12. // 二级树上永远读到 0, 整棵子树只回收了根页, 其余成为永久孤儿页。
  13. //
  14. // 这条缺陷同时污染三个调用方 (它们共用 CollectAllPageIDs):
  15. // - Deleter.releaseIndexPages → DROP INDEX 与批量删除后的旧树重建;
  16. // - TablePage.DropTable → DROP TABLE 回收 B+ 树索引页。
  17. //
  18. // 观测的是 header.NextFreePage (与 CollectAllPageIDs 无关的独立量), 不是函数自己的
  19. // 返回值 —— 否则「漏收」与「漏归还」会互相印证, 用例反而通过。
  20. //
  21. // 判据: 反复 DropIndex + CreateIndex。旧树页若被完整归还, 下一轮重建会复用它们,
  22. // 文件不再增长; 只还根页时, 每轮都要新分配整棵树 → NextFreePage 逐轮上涨
  23. // (正是 bench_10m 里「文件只涨不缩」的直接来源)。
  24. // ============================================================================
  25. func TestDropIndexReclaimsWholeSecondaryTree(t *testing.T) {
  26. const (
  27. rows = 12000 // 二级叶容量 ≈ 1022 条 (entryLen=16) → 12 叶 + 1 根
  28. rounds = 8
  29. )
  30. db := fixOpenDB(t)
  31. tbl, err := db.CreateTable("LEAK", []types.FieldInfo{
  32. {FieldType: types.FieldType_Uint64, FieldName: []byte("ID")},
  33. {FieldType: types.FieldType_Uint64, FieldName: []byte("Tag")},
  34. {FieldType: types.FieldType_Uint64, FieldName: []byte("Score")},
  35. })
  36. if err != nil {
  37. t.Fatalf("create table: %v", err)
  38. }
  39. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  40. t.Fatalf("create pk: %v", err)
  41. }
  42. for i := uint64(1); i <= rows; i++ {
  43. if _, err := tbl.InsertRow([]types.FieldValue{
  44. {FieldName: []byte("ID"), Value: i},
  45. {FieldName: []byte("Tag"), Value: i % 97},
  46. {FieldName: []byte("Score"), Value: i * 13},
  47. }); err != nil {
  48. t.Fatalf("insert %d: %v", i, err)
  49. }
  50. }
  51. // sec 覆盖 Tag 列, 建索引时回填存量行 → 立刻是一棵 12 叶 + 1 根的多级二级树。
  52. if _, err := tbl.CreateIndex("sec", page.IndexTypeBTree, []byte{1}); err != nil {
  53. t.Fatalf("create sec: %v", err)
  54. }
  55. if err := db.Commit(); err != nil {
  56. t.Fatalf("commit: %v", err)
  57. }
  58. var base uint32
  59. for r := 1; r <= rounds; r++ {
  60. start := db.header.NextFreePage
  61. if err := tbl.DropIndex("sec"); err != nil {
  62. t.Fatalf("第 %d 轮 DropIndex: %v", r, err)
  63. }
  64. if _, err := tbl.CreateIndex("sec", page.IndexTypeBTree, []byte{1}); err != nil {
  65. t.Fatalf("第 %d 轮 CreateIndex: %v", r, err)
  66. }
  67. if err := db.Commit(); err != nil {
  68. t.Fatalf("第 %d 轮 Commit: %v", r, err)
  69. }
  70. if r == 1 {
  71. base = db.header.NextFreePage
  72. }
  73. growth := db.header.NextFreePage - base
  74. t.Logf("第 %d 轮 Drop/Create: NextFreePage=%d (本轮 +%d, 较第 1 轮 +%d)",
  75. r, db.header.NextFreePage, db.header.NextFreePage-start, growth)
  76. // 修复前: 每轮漏收 12 叶页 → 稳态每轮至少 +12 (旧树只还了根页)。
  77. // 修复后: 旧树在上一轮 Commit 已还链, 本轮重建全部复用 → 只剩 commit 的常数抖动。
  78. // 阈值取 3/轮 (远小于 12), 前几轮的空闲链余量可能暂时吸收少量泄漏, 故看累计值。
  79. if r > 1 && growth > uint32(3*(r-1)) {
  80. t.Fatalf("二级索引 Drop/Create 第 %d 轮后 NextFreePage 累计多涨 %d 页: "+
  81. "旧树页未被完整归还 (空闲链收不到, 文件只涨不缩)", r, growth)
  82. }
  83. }
  84. }