blob_leak_test.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package core_test
  2. // 回归: 写入失败的行不得泄漏 blob 链页 (Type=9 孤儿页)。
  3. //
  4. // 两条历史泄漏路径:
  5. // 1. serializeRowInternal 里 blob 列先落链, 后续列编码失败 → 链页无人引用;
  6. // 2. indexMeta.InsertRow/InsertRows 唯一冲突报错 → 行没落任何索引, 链页无人引用。
  7. // 修复后这些链页必须回到空闲链 (orphancheck 的 blob 感知口径孤儿数 == 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. "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
  15. )
  16. func blobLeakSchema() []types.FieldInfo {
  17. return []types.FieldInfo{
  18. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  19. {FieldType: types.FieldType_Blob, FieldName: []byte("body")},
  20. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(32), FieldName: []byte("tag")},
  21. }
  22. }
  23. func openBlobLeakDB(t *testing.T, path string) *core.DB {
  24. t.Helper()
  25. db, err := core.Open(path)
  26. if err != nil {
  27. t.Fatal(err)
  28. }
  29. db.SetAutoCommitDelay(0)
  30. tbl, err := db.CreateTable("docs", blobLeakSchema())
  31. if err != nil {
  32. t.Fatal(err)
  33. }
  34. if _, err := tbl.CreateIndex("pk_docs", page.IndexTypePrimary, []byte{0}); err != nil {
  35. t.Fatal(err)
  36. }
  37. return db
  38. }
  39. // 失败行 (序列化阶段: blob 列之后的字符串列超宽) 不得泄漏链页。
  40. func TestBlobChainFreedOnSerializeError(t *testing.T) {
  41. path := t.TempDir() + "/leak1.db"
  42. db := openBlobLeakDB(t, path)
  43. // 先行一行成功数据, 作为"活链"对照。
  44. good, ok := db.FindTable("docs")
  45. if !ok {
  46. t.Fatal("FindTable docs: not found")
  47. }
  48. if _, err := good.InsertRowWithID(1, []types.FieldValue{
  49. {FieldName: []byte("id"), Value: uint64(1)},
  50. {FieldName: []byte("body"), Value: bytes.Repeat([]byte("a"), 40000)},
  51. {FieldName: []byte("tag"), Value: []byte("ok")},
  52. }); err != nil {
  53. t.Fatal(err)
  54. }
  55. // 失败行: blob 列先落链 (40KB), 随后 tag 超宽 → 整行写入中止。
  56. if _, err := good.InsertRowWithID(2, []types.FieldValue{
  57. {FieldName: []byte("id"), Value: uint64(2)},
  58. {FieldName: []byte("body"), Value: bytes.Repeat([]byte("b"), 40000)},
  59. {FieldName: []byte("tag"), Value: bytes.Repeat([]byte("x"), 100)},
  60. }); err == nil {
  61. t.Fatal("超宽 tag 应报错, 实际写入成功")
  62. }
  63. if err := db.Commit(); err != nil {
  64. t.Fatal(err)
  65. }
  66. if err := db.Close(); err != nil {
  67. t.Fatal(err)
  68. }
  69. R, err := orphancheck.Run(path, orphancheck.Options{EngineBlobScan: true})
  70. if err != nil {
  71. t.Fatal(err)
  72. }
  73. if len(R.Orphans) > 0 {
  74. t.Fatalf("失败行泄漏 %d 页: %v (按类型 %v)", len(R.Orphans), idsOf(R.Orphans), R.ByType)
  75. }
  76. if R.BlobPages == 0 {
  77. t.Fatal("活 blob 链页数 = 0 —— 成功行的链也应被认领, 检查走查逻辑")
  78. }
  79. }
  80. // 唯一冲突行 (索引阶段预检报错, 零写入) 不得泄漏链页。
  81. func TestBlobChainFreedOnUniqueConflict(t *testing.T) {
  82. path := t.TempDir() + "/leak2.db"
  83. db := openBlobLeakDB(t, path)
  84. tbl, ok := db.FindTable("docs")
  85. if !ok {
  86. t.Fatal("FindTable docs: not found")
  87. }
  88. if _, err := tbl.CreateIndex("uq_docs_tag", page.IndexTypeBTree, []byte{2}, true); err != nil {
  89. t.Fatal(err)
  90. }
  91. body1 := bytes.Repeat([]byte("a"), 40000)
  92. if _, err := tbl.InsertRowWithID(1, []types.FieldValue{
  93. {FieldName: []byte("id"), Value: uint64(1)},
  94. {FieldName: []byte("body"), Value: body1},
  95. {FieldName: []byte("tag"), Value: []byte("dup")},
  96. }); err != nil {
  97. t.Fatal(err)
  98. }
  99. if err := db.Commit(); err != nil {
  100. t.Fatal(err)
  101. }
  102. // 冲突行: 唯一预检先于任何写入, blob 链已落 → 必须被回收。
  103. if _, err := tbl.InsertRowWithID(2, []types.FieldValue{
  104. {FieldName: []byte("id"), Value: uint64(2)},
  105. {FieldName: []byte("body"), Value: bytes.Repeat([]byte("b"), 40000)},
  106. {FieldName: []byte("tag"), Value: []byte("dup")},
  107. }); err == nil {
  108. t.Fatal("唯一冲突应报错, 实际写入成功")
  109. }
  110. // 批量路径同样不得泄漏。
  111. if _, err := tbl.InsertRowsWithIDs([]uint64{3}, [][]types.FieldValue{{
  112. {FieldName: []byte("id"), Value: uint64(3)},
  113. {FieldName: []byte("body"), Value: bytes.Repeat([]byte("c"), 40000)},
  114. {FieldName: []byte("tag"), Value: []byte("dup")},
  115. }}); err == nil {
  116. t.Fatal("批量唯一冲突应报错, 实际写入成功")
  117. }
  118. if err := db.Commit(); err != nil {
  119. t.Fatal(err)
  120. }
  121. // 先行行的 blob 必须完好 (回收不得误伤活链)。
  122. got, err := tbl.BlobRead(types.BlobRef{
  123. FirstPageID: firstPageOf(t, tbl, 1), TotalLen: uint32(len(body1)),
  124. }, types.CompressNone)
  125. if err != nil {
  126. t.Fatalf("读回先行行的 blob: %v", err)
  127. }
  128. if !bytes.Equal(got, body1) {
  129. t.Fatalf("先行行的 blob 内容被损毁: 读回 %d 字节, 期望 %d", len(got), len(body1))
  130. }
  131. if err := db.Close(); err != nil {
  132. t.Fatal(err)
  133. }
  134. R, err := orphancheck.Run(path, orphancheck.Options{EngineBlobScan: true})
  135. if err != nil {
  136. t.Fatal(err)
  137. }
  138. if len(R.Orphans) > 0 {
  139. t.Fatalf("唯一冲突行泄漏 %d 页: %v (按类型 %v)", len(R.Orphans), idsOf(R.Orphans), R.ByType)
  140. }
  141. }
  142. // firstPageOf 取一行 body 列的链首页 (经行存储读回 BlobRef)。
  143. func firstPageOf(t *testing.T, tbl *core.Table, pkID uint64) uint32 {
  144. t.Helper()
  145. vals, err := tbl.FindByPK(pkID)
  146. if err != nil {
  147. t.Fatalf("FindByPK(%d): %v", pkID, err)
  148. }
  149. for _, fv := range vals {
  150. if string(fv.FieldName) == "body" {
  151. ref, ok := fv.Value.(types.BlobRef)
  152. if !ok {
  153. t.Fatalf("body 列类型 %T, 期望 types.BlobRef", fv.Value)
  154. }
  155. return ref.FirstPageID
  156. }
  157. }
  158. t.Fatal("行里没有 body 列")
  159. return 0
  160. }
  161. func idsOf(ps []orphancheck.PageInfo) []uint32 {
  162. out := make([]uint32, len(ps))
  163. for i, p := range ps {
  164. out[i] = p.ID
  165. }
  166. return out
  167. }