|
|
@@ -0,0 +1,187 @@
|
|
|
+package core_test
|
|
|
+
|
|
|
+// 回归: 写入失败的行不得泄漏 blob 链页 (Type=9 孤儿页)。
|
|
|
+//
|
|
|
+// 两条历史泄漏路径:
|
|
|
+// 1. serializeRowInternal 里 blob 列先落链, 后续列编码失败 → 链页无人引用;
|
|
|
+// 2. indexMeta.InsertRow/InsertRows 唯一冲突报错 → 行没落任何索引, 链页无人引用。
|
|
|
+// 修复后这些链页必须回到空闲链 (orphancheck 的 blob 感知口径孤儿数 == 0)。
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "git.ycl.pub/yl32940/TsDB2/DB/core"
|
|
|
+ "git.ycl.pub/yl32940/TsDB2/DB/core/page"
|
|
|
+ "git.ycl.pub/yl32940/TsDB2/DB/core/types"
|
|
|
+ "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
|
|
|
+)
|
|
|
+
|
|
|
+func blobLeakSchema() []types.FieldInfo {
|
|
|
+ return []types.FieldInfo{
|
|
|
+ {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
|
|
|
+ {FieldType: types.FieldType_Blob, FieldName: []byte("body")},
|
|
|
+ {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(32), FieldName: []byte("tag")},
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func openBlobLeakDB(t *testing.T, path string) *core.DB {
|
|
|
+ t.Helper()
|
|
|
+ db, err := core.Open(path)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ db.SetAutoCommitDelay(0)
|
|
|
+ tbl, err := db.CreateTable("docs", blobLeakSchema())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if _, err := tbl.CreateIndex("pk_docs", page.IndexTypePrimary, []byte{0}); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ return db
|
|
|
+}
|
|
|
+
|
|
|
+// 失败行 (序列化阶段: blob 列之后的字符串列超宽) 不得泄漏链页。
|
|
|
+func TestBlobChainFreedOnSerializeError(t *testing.T) {
|
|
|
+ path := t.TempDir() + "/leak1.db"
|
|
|
+ db := openBlobLeakDB(t, path)
|
|
|
+
|
|
|
+ // 先行一行成功数据, 作为"活链"对照。
|
|
|
+ good, ok := db.FindTable("docs")
|
|
|
+ if !ok {
|
|
|
+ t.Fatal("FindTable docs: not found")
|
|
|
+ }
|
|
|
+ if _, err := good.InsertRowWithID(1, []types.FieldValue{
|
|
|
+ {FieldName: []byte("id"), Value: uint64(1)},
|
|
|
+ {FieldName: []byte("body"), Value: bytes.Repeat([]byte("a"), 40000)},
|
|
|
+ {FieldName: []byte("tag"), Value: []byte("ok")},
|
|
|
+ }); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 失败行: blob 列先落链 (40KB), 随后 tag 超宽 → 整行写入中止。
|
|
|
+ if _, err := good.InsertRowWithID(2, []types.FieldValue{
|
|
|
+ {FieldName: []byte("id"), Value: uint64(2)},
|
|
|
+ {FieldName: []byte("body"), Value: bytes.Repeat([]byte("b"), 40000)},
|
|
|
+ {FieldName: []byte("tag"), Value: bytes.Repeat([]byte("x"), 100)},
|
|
|
+ }); err == nil {
|
|
|
+ t.Fatal("超宽 tag 应报错, 实际写入成功")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := db.Commit(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := db.Close(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ R, err := orphancheck.Run(path, orphancheck.Options{EngineBlobScan: true})
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(R.Orphans) > 0 {
|
|
|
+ t.Fatalf("失败行泄漏 %d 页: %v (按类型 %v)", len(R.Orphans), idsOf(R.Orphans), R.ByType)
|
|
|
+ }
|
|
|
+ if R.BlobPages == 0 {
|
|
|
+ t.Fatal("活 blob 链页数 = 0 —— 成功行的链也应被认领, 检查走查逻辑")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 唯一冲突行 (索引阶段预检报错, 零写入) 不得泄漏链页。
|
|
|
+func TestBlobChainFreedOnUniqueConflict(t *testing.T) {
|
|
|
+ path := t.TempDir() + "/leak2.db"
|
|
|
+ db := openBlobLeakDB(t, path)
|
|
|
+ tbl, ok := db.FindTable("docs")
|
|
|
+ if !ok {
|
|
|
+ t.Fatal("FindTable docs: not found")
|
|
|
+ }
|
|
|
+ if _, err := tbl.CreateIndex("uq_docs_tag", page.IndexTypeBTree, []byte{2}, true); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ body1 := bytes.Repeat([]byte("a"), 40000)
|
|
|
+ if _, err := tbl.InsertRowWithID(1, []types.FieldValue{
|
|
|
+ {FieldName: []byte("id"), Value: uint64(1)},
|
|
|
+ {FieldName: []byte("body"), Value: body1},
|
|
|
+ {FieldName: []byte("tag"), Value: []byte("dup")},
|
|
|
+ }); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := db.Commit(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 冲突行: 唯一预检先于任何写入, blob 链已落 → 必须被回收。
|
|
|
+ if _, err := tbl.InsertRowWithID(2, []types.FieldValue{
|
|
|
+ {FieldName: []byte("id"), Value: uint64(2)},
|
|
|
+ {FieldName: []byte("body"), Value: bytes.Repeat([]byte("b"), 40000)},
|
|
|
+ {FieldName: []byte("tag"), Value: []byte("dup")},
|
|
|
+ }); err == nil {
|
|
|
+ t.Fatal("唯一冲突应报错, 实际写入成功")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量路径同样不得泄漏。
|
|
|
+ if _, err := tbl.InsertRowsWithIDs([]uint64{3}, [][]types.FieldValue{{
|
|
|
+ {FieldName: []byte("id"), Value: uint64(3)},
|
|
|
+ {FieldName: []byte("body"), Value: bytes.Repeat([]byte("c"), 40000)},
|
|
|
+ {FieldName: []byte("tag"), Value: []byte("dup")},
|
|
|
+ }}); err == nil {
|
|
|
+ t.Fatal("批量唯一冲突应报错, 实际写入成功")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := db.Commit(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先行行的 blob 必须完好 (回收不得误伤活链)。
|
|
|
+ got, err := tbl.BlobRead(types.BlobRef{
|
|
|
+ FirstPageID: firstPageOf(t, tbl, 1), TotalLen: uint32(len(body1)),
|
|
|
+ }, types.CompressNone)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("读回先行行的 blob: %v", err)
|
|
|
+ }
|
|
|
+ if !bytes.Equal(got, body1) {
|
|
|
+ t.Fatalf("先行行的 blob 内容被损毁: 读回 %d 字节, 期望 %d", len(got), len(body1))
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := db.Close(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ R, err := orphancheck.Run(path, orphancheck.Options{EngineBlobScan: true})
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(R.Orphans) > 0 {
|
|
|
+ t.Fatalf("唯一冲突行泄漏 %d 页: %v (按类型 %v)", len(R.Orphans), idsOf(R.Orphans), R.ByType)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// firstPageOf 取一行 body 列的链首页 (经行存储读回 BlobRef)。
|
|
|
+func firstPageOf(t *testing.T, tbl *core.Table, pkID uint64) uint32 {
|
|
|
+ t.Helper()
|
|
|
+ vals, err := tbl.FindByPK(pkID)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("FindByPK(%d): %v", pkID, err)
|
|
|
+ }
|
|
|
+ for _, fv := range vals {
|
|
|
+ if string(fv.FieldName) == "body" {
|
|
|
+ ref, ok := fv.Value.(types.BlobRef)
|
|
|
+ if !ok {
|
|
|
+ t.Fatalf("body 列类型 %T, 期望 types.BlobRef", fv.Value)
|
|
|
+ }
|
|
|
+ return ref.FirstPageID
|
|
|
+ }
|
|
|
+ }
|
|
|
+ t.Fatal("行里没有 body 列")
|
|
|
+ return 0
|
|
|
+}
|
|
|
+
|
|
|
+func idsOf(ps []orphancheck.PageInfo) []uint32 {
|
|
|
+ out := make([]uint32, len(ps))
|
|
|
+ for i, p := range ps {
|
|
|
+ out[i] = p.ID
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|