blobwalk_test.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. package main
  2. // 回归: 带 blob 列的库, 结构审计不得把活 blob 链页报成孤儿页。
  3. //
  4. // 背景 (DockerManager 实测): 迁移后的库 analyzer 报"44 页既不属于任何表/索引,
  5. // 也不在空闲链上" —— 那 44 页是行内 BlobRef 指向的活链页 (Type=9), 早期
  6. // analyzer 的可达根不含 blob 链, 整链误报。本测试建一张带 blob 列的表
  7. // (含跨页大 blob), 跑 runAuditStructural, 要求 Unaccounted == 0。
  8. import (
  9. "bytes"
  10. "testing"
  11. "git.ycl.pub/yl32940/TsDB2/DB/core"
  12. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  13. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  14. )
  15. func seedBlobDB(t *testing.T) string {
  16. t.Helper()
  17. path := t.TempDir() + "/blob.db"
  18. db, err := core.Open(path)
  19. if err != nil {
  20. t.Fatal(err)
  21. }
  22. db.SetAutoCommitDelay(0)
  23. tbl, err := db.CreateTable("docs", []types.FieldInfo{
  24. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  25. {FieldType: types.FieldType_Blob, FieldName: []byte("body")},
  26. })
  27. if err != nil {
  28. t.Fatal(err)
  29. }
  30. if _, err := tbl.CreateIndex("pk_docs", page.IndexTypePrimary, []byte{0}); err != nil {
  31. t.Fatal(err)
  32. }
  33. // 小 blob (单页链) + 大 blob (跨页链, 确保多页链也被认领)。
  34. big := bytes.Repeat([]byte("0123456789abcdef"), 8*1024) // 128KB ≈ 8 个链页
  35. rows := [][]byte{
  36. []byte("hello blob"),
  37. big,
  38. bytes.Repeat([]byte("z"), 20000), // 略超单页数据区, 强制跨页
  39. }
  40. for i, body := range rows {
  41. if _, err := tbl.InsertRowWithID(uint64(i+1), []types.FieldValue{
  42. {FieldName: []byte("id"), Value: uint64(i + 1)},
  43. {FieldName: []byte("body"), Value: body},
  44. }); err != nil {
  45. t.Fatal(err)
  46. }
  47. }
  48. if err := db.Commit(); err != nil {
  49. t.Fatal(err)
  50. }
  51. if err := db.Close(); err != nil {
  52. t.Fatal(err)
  53. }
  54. return path
  55. }
  56. func TestAuditBlobChainsReachable(t *testing.T) {
  57. path := seedBlobDB(t)
  58. if err := useDB(path); err != nil {
  59. t.Fatalf("useDB: %v", err)
  60. }
  61. invalidateAudit()
  62. mu.Lock()
  63. fp = path
  64. rHdr()
  65. tabs = pTabs()
  66. mu.Unlock()
  67. R, _, _, _, _ := runAuditStructural()
  68. if R.Reach.Wal > 0 {
  69. t.Skipf("库带着未恢复 WAL (%d 页), 页归属不完整, 本断言不适用", R.Reach.Wal)
  70. }
  71. if R.Reach.Unaccounted != 0 {
  72. for i := range R.Tables { t.Logf("表 %s BlobChains=%d BlobPages=%d", R.Tables[i].Name, R.Tables[i].BlobChains, R.Tables[i].BlobPages) }
  73. t.Fatalf("活 blob 链页被误报成孤儿: Unaccounted=%d byType=%v ids=%v",
  74. R.Reach.Unaccounted, R.Reach.UnaccountedByType, R.Reach.UnaccountedIDs)
  75. }
  76. if got := R.Reach.ByRole["blob"]; got <= 0 {
  77. t.Fatalf("ByRole[blob] = %d, 期望 > 0 —— blob 链根本没被走查", got)
  78. }
  79. var ta *TableAudit
  80. for i := range R.Tables {
  81. if R.Tables[i].Name == "docs" {
  82. ta = &R.Tables[i]
  83. }
  84. }
  85. if ta == nil {
  86. t.Fatal("审计结果里没有 docs 表")
  87. }
  88. if ta.BlobChains != 3 {
  89. t.Fatalf("BlobChains = %d, 期望 3 (每行一条链)", ta.BlobChains)
  90. }
  91. if ta.BlobPages < 10 {
  92. t.Fatalf("BlobPages = %d, 期望 >= 10 (128KB 大 blob 单独就 ~8 页)", ta.BlobPages)
  93. }
  94. // 释放全局句柄, 否则 Windows 下 t.TempDir 清理删不掉文件
  95. fileMu.Lock()
  96. if dbf != nil {
  97. dbf.Close()
  98. dbf = nil
  99. }
  100. fileMu.Unlock()
  101. }