| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- package core
- import (
- "testing"
- "git.ycl.pub/yl32940/TsDB2/DB/core/page"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- )
- // ============================================================================
- // 端到端页回收回归: DROP INDEX 必须把整棵二级索引树还给空闲链。
- //
- // 缺陷 (千万行审计发现): btree.CollectAllPageIDs 下探内部节点时统一按主键布局
- // 读「偏移 8..12」的子页号, 而二级 B+ 树的子页号在条目**末 4 字节**。结果 BFS 在
- // 二级树上永远读到 0, 整棵子树只回收了根页, 其余成为永久孤儿页。
- //
- // 这条缺陷同时污染三个调用方 (它们共用 CollectAllPageIDs):
- // - Deleter.releaseIndexPages → DROP INDEX 与批量删除后的旧树重建;
- // - TablePage.DropTable → DROP TABLE 回收 B+ 树索引页。
- //
- // 观测的是 header.NextFreePage (与 CollectAllPageIDs 无关的独立量), 不是函数自己的
- // 返回值 —— 否则「漏收」与「漏归还」会互相印证, 用例反而通过。
- //
- // 判据: 反复 DropIndex + CreateIndex。旧树页若被完整归还, 下一轮重建会复用它们,
- // 文件不再增长; 只还根页时, 每轮都要新分配整棵树 → NextFreePage 逐轮上涨
- // (正是 bench_10m 里「文件只涨不缩」的直接来源)。
- // ============================================================================
- func TestDropIndexReclaimsWholeSecondaryTree(t *testing.T) {
- const (
- rows = 12000 // 二级叶容量 ≈ 1022 条 (entryLen=16) → 12 叶 + 1 根
- rounds = 8
- )
- db := fixOpenDB(t)
- tbl, err := db.CreateTable("LEAK", []types.FieldInfo{
- {FieldType: types.FieldType_Uint64, FieldName: []byte("ID")},
- {FieldType: types.FieldType_Uint64, FieldName: []byte("Tag")},
- {FieldType: types.FieldType_Uint64, FieldName: []byte("Score")},
- })
- if err != nil {
- t.Fatalf("create table: %v", err)
- }
- if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
- t.Fatalf("create pk: %v", err)
- }
- for i := uint64(1); i <= rows; i++ {
- if _, err := tbl.InsertRow([]types.FieldValue{
- {FieldName: []byte("ID"), Value: i},
- {FieldName: []byte("Tag"), Value: i % 97},
- {FieldName: []byte("Score"), Value: i * 13},
- }); err != nil {
- t.Fatalf("insert %d: %v", i, err)
- }
- }
- // sec 覆盖 Tag 列, 建索引时回填存量行 → 立刻是一棵 12 叶 + 1 根的多级二级树。
- if _, err := tbl.CreateIndex("sec", page.IndexTypeBTree, []byte{1}); err != nil {
- t.Fatalf("create sec: %v", err)
- }
- if err := db.Commit(); err != nil {
- t.Fatalf("commit: %v", err)
- }
- var base uint32
- for r := 1; r <= rounds; r++ {
- start := db.header.NextFreePage
- if err := tbl.DropIndex("sec"); err != nil {
- t.Fatalf("第 %d 轮 DropIndex: %v", r, err)
- }
- if _, err := tbl.CreateIndex("sec", page.IndexTypeBTree, []byte{1}); err != nil {
- t.Fatalf("第 %d 轮 CreateIndex: %v", r, err)
- }
- if err := db.Commit(); err != nil {
- t.Fatalf("第 %d 轮 Commit: %v", r, err)
- }
- if r == 1 {
- base = db.header.NextFreePage
- }
- growth := db.header.NextFreePage - base
- t.Logf("第 %d 轮 Drop/Create: NextFreePage=%d (本轮 +%d, 较第 1 轮 +%d)",
- r, db.header.NextFreePage, db.header.NextFreePage-start, growth)
- // 修复前: 每轮漏收 12 叶页 → 稳态每轮至少 +12 (旧树只还了根页)。
- // 修复后: 旧树在上一轮 Commit 已还链, 本轮重建全部复用 → 只剩 commit 的常数抖动。
- // 阈值取 3/轮 (远小于 12), 前几轮的空闲链余量可能暂时吸收少量泄漏, 故看累计值。
- if r > 1 && growth > uint32(3*(r-1)) {
- t.Fatalf("二级索引 Drop/Create 第 %d 轮后 NextFreePage 累计多涨 %d 页: "+
- "旧树页未被完整归还 (空闲链收不到, 文件只涨不缩)", r, growth)
- }
- }
- }
|