datapage_compact_test.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. package core
  2. // 本文件回答一个具体的运维问题: 单页内"删得多 / 改得多"时, 引擎会在什么时候把这一页
  3. // 就地整理一遍? 四个事实必须能被这些用例单独复现:
  4. //
  5. // 1. 删除本身【不整理】: 只打墓碑 + 累加 deadBytes, 页内物理行数 (Count, 含墓碑) 不变。
  6. // 墓碑占用的行区在此期间只增不减, 也不会被复用 (allocSlot 只认空槽, 墓碑行的槽位
  7. // 仍非 0), 于是这一页会提前判满。
  8. // 2. 整理发生在"要为新行开新页"的那一刻: 主键/时序插入路径在末页装不下时的下一步不是
  9. // 直接开页, 而是先 datapage.CompactForInsert —— 墓碑占比达到 CompactDeadRatio
  10. // (默认 0.4) 且"整理后确实装得下"时就地整理并重试写入。配置 <=0 即关闭自动整理,
  11. // 行为退回"装不下就开新页"。
  12. // 3. 整理是【就地】且【槽位号不变】的: 页 ID 不变、活行槽位号不变 —— 主键叶条目存的是
  13. // (dpID, slot), 一变就全指错行, 所以槽位稳定是硬约束。Compact 自身【不落盘】,
  14. // 落盘由调用方 (CompactForInsert / 手工调用方) 承担。
  15. // 4. 更新分两条路: 等长更新走 UpdateBySlot 原地覆盖, 既不产生墓碑也没有"整理"需求;
  16. // 变长 (v9 字符串列长出来) 装不下时走 RebuildWithSlot 页内重建 —— 它会顺带把墓碑
  17. // 一并回收, 但触发条件是"装不下", 而不是阈值。
  18. import (
  19. "encoding/binary"
  20. "strings"
  21. "testing"
  22. tsindex "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  23. "git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
  24. "git.ycl.pub/yl32940/TsDB2/DB/core/index/datapage"
  25. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  26. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  27. )
  28. // ======================== 测试脚手架 ========================
  29. // cmpBE 把 pkID 编成 B+ 树叶键的 8 字节 BigEndian 形式 (与 PrimaryHandler 同一编码)。
  30. func cmpBE(pk uint64) []byte {
  31. b := make([]byte, 8)
  32. binary.BigEndian.PutUint64(b, pk)
  33. return b
  34. }
  35. // cmpNewTable 建一张"小行、多行"的主键表: 8B 主键 + 1B + 8B ⇒ 单行约 37 字节,
  36. // 一页能放几百行 —— 这样"单页内删掉一半"才容易构造出来。
  37. func cmpNewTable(t *testing.T, db *DB, name string) *Table {
  38. t.Helper()
  39. tbl, err := db.CreateTable(name, []types.FieldInfo{
  40. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  41. {FieldType: types.FieldType_Uint8, FieldName: []byte("flag")},
  42. {FieldType: types.FieldType_Uint64, FieldName: []byte("v")},
  43. })
  44. if err != nil {
  45. t.Fatalf("建表 %s: %v", name, err)
  46. }
  47. // 主键要落在 8 字节定长列上 (列 0): 时序/主键索引都把首列当键。
  48. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  49. t.Fatalf("建主键索引: %v", err)
  50. }
  51. return tbl
  52. }
  53. // cmpNewVarTable 建一张带变长 (字符串) 列的表, 用来区分"等长更新"与"变长更新"。
  54. func cmpNewVarTable(t *testing.T, db *DB, name string) *Table {
  55. t.Helper()
  56. tbl, err := db.CreateTable(name, []types.FieldInfo{
  57. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  58. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(64), FieldName: []byte("s")},
  59. })
  60. if err != nil {
  61. t.Fatalf("建表 %s: %v", name, err)
  62. }
  63. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  64. t.Fatalf("建主键索引: %v", err)
  65. }
  66. return tbl
  67. }
  68. // cmpInsert 插入 n 行, 返回它们的 pkID。
  69. // 用 AllocPKID + InsertRowWithID (而不是让 InsertRow 自己发号) 是为了让行内的 id 列
  70. // 与真实 pkID 逐字节一致 —— 否则叶条目校验行首 8B 时会判成"陈旧槽位"而读不到行。
  71. func cmpInsert(t *testing.T, tbl *Table, n int) []uint64 {
  72. t.Helper()
  73. pks := make([]uint64, 0, n)
  74. for i := 0; i < n; i++ {
  75. pk := tbl.AllocPKID()
  76. if _, err := tbl.InsertRowWithID(pk, []types.FieldValue{
  77. {FieldName: []byte("id"), Value: pk},
  78. {FieldName: []byte("flag"), Value: uint8(i)},
  79. {FieldName: []byte("v"), Value: uint64(i)},
  80. }); err != nil {
  81. t.Fatalf("插入第 %d 行: %v", i, err)
  82. }
  83. pks = append(pks, pk)
  84. }
  85. return pks
  86. }
  87. // cmpPrimaryMeta 取表的主键索引元信息 (这些用例只建一个 Primary 索引)。
  88. func cmpPrimaryMeta(t *testing.T, tbl *Table) *IndexMeta {
  89. t.Helper()
  90. return cmpMetaOf(t, tbl, page.IndexTypePrimary)
  91. }
  92. // cmpMetaOf 按索引类型取元信息 (返回的是切片元素的地址, 所以元信息里的
  93. // EntryPage 被更新 —— 例如 B+ 树分裂改根 —— 时这里读到的也是最新值)。
  94. func cmpMetaOf(t *testing.T, tbl *Table, indexType page.IndexType) *IndexMeta {
  95. t.Helper()
  96. metas := tbl.IndexMeta().metas
  97. for i := range metas {
  98. if metas[i].IndexType == indexType {
  99. return &metas[i]
  100. }
  101. }
  102. t.Fatalf("表上找不到类型为 %d 的索引", indexType)
  103. return nil
  104. }
  105. // cmpDataPages 返回主键索引当前引用到的全部 DataPage。
  106. func cmpDataPages(t *testing.T, tbl *Table, meta *IndexMeta) []uint32 {
  107. t.Helper()
  108. return btree.GetDataPageIDs(tbl.handle, meta.EntryPage)
  109. }
  110. // cmpLoad 从盘 (缓冲池) 重新载入一页 —— 断言必须看【落盘后】的状态, 而不是内存里的对象。
  111. func cmpLoad(t *testing.T, tbl *Table, dpID uint32) *datapage.DataPage {
  112. t.Helper()
  113. dp, err := datapage.Load(tbl.handle, dpID)
  114. if err != nil {
  115. t.Fatalf("载入数据页 %d: %v", dpID, err)
  116. }
  117. return dp
  118. }
  119. // cmpOnlyPage 断言当前只有一页并返回它 —— "单页内操作"是本文件所有用例的前提。
  120. func cmpOnlyPage(t *testing.T, tbl *Table, meta *IndexMeta) uint32 {
  121. t.Helper()
  122. dpIDs := cmpDataPages(t, tbl, meta)
  123. if len(dpIDs) != 1 {
  124. t.Fatalf("前置条件: 数据应当全落在一页, 实际 %d 页", len(dpIDs))
  125. }
  126. return dpIDs[0]
  127. }
  128. // cmpFreezeThreshold 冻结整理阈值, 并在用例结束后还原 —— CompactDeadRatio 是包级可变量,
  129. // 不还原会污染同包其它用例。
  130. func cmpFreezeThreshold(t *testing.T, ratio float64) {
  131. t.Helper()
  132. saved := datapage.CompactDeadRatio
  133. t.Cleanup(func() { datapage.SetCompactDeadRatio(saved) })
  134. datapage.SetCompactDeadRatio(ratio)
  135. }
  136. // cmpU64 把读回的 8 字节列取值归一成 uint64 (DecodeValue 的具体类型可能变,
  137. // 但"读出来的数就是写进去的数"这一条不能变)。
  138. func cmpU64(t *testing.T, fv types.FieldValue) uint64 {
  139. t.Helper()
  140. switch v := fv.Value.(type) {
  141. case uint64:
  142. return v
  143. case int64:
  144. return uint64(v)
  145. case uint32:
  146. return uint64(v)
  147. case int32:
  148. return uint64(v)
  149. default:
  150. t.Fatalf("列 %q 的值类型是 %T, 期望整数类型", fv.FieldName, fv.Value)
  151. return 0
  152. }
  153. }
  154. // cmpStr 把读回的字符串列取值归一成 string (DecodeValue 给出的是 []byte)。
  155. func cmpStr(t *testing.T, fv types.FieldValue) string {
  156. t.Helper()
  157. switch v := fv.Value.(type) {
  158. case string:
  159. return v
  160. case []byte:
  161. return string(v)
  162. default:
  163. t.Fatalf("列 %q 的值类型是 %T, 期望字符串家族", fv.FieldName, fv.Value)
  164. return ""
  165. }
  166. }
  167. // ======================== 用例 ========================
  168. // TestDataPageDeleteLeavesTombstonesOnly 钉死事实 1:
  169. // 删除只打墓碑 —— 即使墓碑占比已远超整理阈值, 删除本身也不会重排这一页
  170. // (整理被推迟到"要开新页"的那一刻, 见 TestDataPageCompactsBeforeOpeningNewPage)。
  171. func TestDataPageDeleteLeavesTombstonesOnly(t *testing.T) {
  172. cmpFreezeThreshold(t, 0.4)
  173. db := fixOpenDB(t)
  174. tbl := cmpNewTable(t, db, "CMPDEL")
  175. meta := cmpPrimaryMeta(t, tbl)
  176. const total, live = 200, 80
  177. pks := cmpInsert(t, tbl, total)
  178. dpID := cmpOnlyPage(t, tbl, meta)
  179. if got := cmpLoad(t, tbl, dpID).Count(); got != uint32(total) {
  180. t.Fatalf("前置条件: 页内应有 %d 行, 实际 %d", total, got)
  181. }
  182. for _, pk := range pks[live:] {
  183. if err := tbl.DeleteRowByPK(pk); err != nil {
  184. t.Fatalf("删除 pkID=%d: %v", pk, err)
  185. }
  186. }
  187. dp := cmpLoad(t, tbl, dpID)
  188. if dp.DeadBytes() == 0 {
  189. t.Fatal("删除后页内应有失效 (墓碑) 字节, 实际 0 —— 删除没走到软删除")
  190. }
  191. if r := dp.DeadRatio(); r < datapage.CompactDeadRatio {
  192. t.Fatalf("前置条件: 墓碑占比 %.2f 应已越过整理阈值 %.2f", r, datapage.CompactDeadRatio)
  193. }
  194. // 键断言: 墓碑占比都过阈值了, 页内物理行数却还是删除前的数字 —— 删除没有重排页。
  195. // (整理被推迟到"要开新页"的那一刻: 删除时整理代价确定而收益未知, 开页前整理才划算。)
  196. if dp.Count() != uint32(total) {
  197. t.Fatalf("墓碑占比已过阈值 %.2f, 但页内物理行数从 %d 变成 %d —— 删除路径在重排页",
  198. datapage.CompactDeadRatio, total, dp.Count())
  199. }
  200. // 删除不应换页, 更不应把页回收 (还有活行)
  201. if dpIDs := cmpDataPages(t, tbl, meta); len(dpIDs) != 1 || dpIDs[0] != dpID {
  202. t.Fatalf("删除不应换页: 期望 [%d], 实际 %v", dpID, dpIDs)
  203. }
  204. // 活行照旧可读: 墓碑只影响被删掉的那些行
  205. for _, pk := range pks[:live] {
  206. if _, err := tbl.FindByPK(pk); err != nil {
  207. t.Fatalf("活行 pkID=%d 应当仍可读: %v", pk, err)
  208. }
  209. }
  210. // 被删的行则查不到
  211. for _, pk := range pks[live:] {
  212. if _, err := tbl.FindByPK(pk); err == nil {
  213. t.Errorf("已删除的 pkID=%d 不应还能读到", pk)
  214. }
  215. }
  216. }
  217. // TestDataPageManualCompactKeepsPageAndSlots 钉死事实 2:
  218. // 手动整理是【就地】的 —— 页 ID 不变、活行槽位号不变 (所以主键叶条目一个字都不用改),
  219. // 墓碑空间全部回收; 但 Compact 自己不落盘, 必须由调用方 Save。
  220. func TestDataPageManualCompactKeepsPageAndSlots(t *testing.T) {
  221. cmpFreezeThreshold(t, 0.4)
  222. db := fixOpenDB(t)
  223. tbl := cmpNewTable(t, db, "CMPCMP")
  224. meta := cmpPrimaryMeta(t, tbl)
  225. const total, live = 200, 60
  226. pks := cmpInsert(t, tbl, total)
  227. dpID := cmpOnlyPage(t, tbl, meta)
  228. // 记下每个活行在主键叶里的定位 (dpID, slot): 整理前后必须逐字不变。
  229. type loc struct {
  230. dp uint32
  231. slot uint16
  232. }
  233. before := make(map[uint64]loc, live)
  234. for _, pk := range pks[:live] {
  235. gotDP, slot, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(pk), tbl.handle)
  236. if !ok || gotDP != dpID {
  237. t.Fatalf("前置条件: pkID=%d 的叶条目应指向数据页 %d, 实际 (%d, ok=%v)", pk, dpID, gotDP, ok)
  238. }
  239. before[pk] = loc{gotDP, slot}
  240. }
  241. for _, pk := range pks[live:] {
  242. if err := tbl.DeleteRowByPK(pk); err != nil {
  243. t.Fatalf("删除 pkID=%d: %v", pk, err)
  244. }
  245. }
  246. if r := cmpLoad(t, tbl, dpID).DeadRatio(); r < datapage.CompactDeadRatio {
  247. t.Fatalf("前置条件: 墓碑占比 %.2f 未越过阈值 %.2f", r, datapage.CompactDeadRatio)
  248. }
  249. dp := cmpLoad(t, tbl, dpID)
  250. if !dp.MaybeCompact() {
  251. t.Fatal("墓碑占比已越过阈值, MaybeCompact 应当动手整理")
  252. }
  253. dp.Save() // Compact 只改内存缓冲, 落盘是调用方的责任 (见 datapage.Compact)
  254. re := cmpLoad(t, tbl, dpID)
  255. if re.DeadBytes() != 0 {
  256. t.Errorf("整理后失效字节应归零, 实际 %d", re.DeadBytes())
  257. }
  258. if re.Count() != uint32(live) {
  259. t.Errorf("整理后页内物理行数应等于活行数 %d, 实际 %d", live, re.Count())
  260. }
  261. if dpIDs := cmpDataPages(t, tbl, meta); len(dpIDs) != 1 || dpIDs[0] != dpID {
  262. t.Errorf("整理应当就地完成: 期望页集合 [%d], 实际 %v", dpID, dpIDs)
  263. }
  264. for _, pk := range pks[:live] {
  265. gotDP, slot, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(pk), tbl.handle)
  266. if !ok || gotDP != before[pk].dp || slot != before[pk].slot {
  267. t.Errorf("pkID=%d 的定位从 (%d,%d) 变成 (%d,%d, ok=%v) —— 叶条目与槽位对不上了",
  268. pk, before[pk].dp, before[pk].slot, gotDP, slot, ok)
  269. continue
  270. }
  271. // 行的内容也要对: 整理是搬字节, 搬错行会在这里露出来 (v 列写的就是 i)
  272. row, err := tbl.FindByPK(pk)
  273. if err != nil {
  274. t.Errorf("整理后 pkID=%d 读不回: %v", pk, err)
  275. continue
  276. }
  277. if len(row) == 0 || cmpU64(t, row[len(row)-1]) != uint64(pk-1) {
  278. t.Errorf("整理后 pkID=%d 的 v 列不对: %v", pk, row)
  279. }
  280. }
  281. // 墓碑行的叶条目已被摘除, 不应再查得到
  282. for _, pk := range pks[live:] {
  283. if _, _, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(pk), tbl.handle); ok {
  284. t.Errorf("已删除的 pkID=%d 不应还能在叶里查到", pk)
  285. }
  286. }
  287. }
  288. // TestDataPageCompactsBeforeOpeningNewPage 覆盖事实 2 (A/B 对照):
  289. // 先把第一页写满 (再写一行就要开新页), 再把它删到只剩 keep 行活行 —— 此刻第一页"行区已满"
  290. // 且几乎全是墓碑, 下一次写入正是引擎做"整理还是开新页"抉择的时刻:
  291. // - 默认阈值 (0.4): 先就地整理, 探针行仍写进第一页, 墓碑清零, 这一页随后还能吃下大量行;
  292. // - 阈值配成 0 (关闭): 探针行落到别的页, 第一页永远拖着那批墓碑。
  293. func TestDataPageCompactsBeforeOpeningNewPage(t *testing.T) {
  294. db := fixOpenDB(t)
  295. const keep = 10 // 第一页保留的活行数 (删完墓碑占比 ≈0.97)
  296. type outcome struct {
  297. firstDP uint32 // 第一页 (被删到只剩 keep 行的那一页) 的页 ID
  298. probePage uint32 // 探针行落在哪一页
  299. pages int // 探针写入后的总页数
  300. firstRows uint32 // 第一页物理行数
  301. firstDead uint32 // 第一页墓碑字节
  302. extra int // 之后第一页还能吃下多少行 (换页前)
  303. }
  304. run := func(name string, ratio float64) outcome {
  305. cmpFreezeThreshold(t, ratio) // 每组独立设定阈值, 用例结束时自动还原
  306. tbl := cmpNewTable(t, db, name)
  307. meta := cmpPrimaryMeta(t, tbl)
  308. // 1) 一直写到出现第二页: 第一页至此装满 (pkID 1..fill-1 都在第一页里)
  309. fill := 0
  310. for {
  311. cmpInsert(t, tbl, 1)
  312. fill++
  313. if len(cmpDataPages(t, tbl, meta)) > 1 {
  314. break
  315. }
  316. }
  317. pageIDs := cmpDataPages(t, tbl, meta)
  318. firstDP := pageIDs[0]
  319. firstRows := cmpLoad(t, tbl, firstDP).Count()
  320. if firstRows == 0 || firstRows != uint32(fill-1) {
  321. t.Fatalf("%s: 前置条件不成立: 第一页行数 %d, 期望 %d", name, firstRows, fill-1)
  322. }
  323. // 2) 删掉溢出的那一行: 它独占的第二页随之被回收 (AllDeleted → FreePage),
  324. // "当前追加页"又回到第一页 —— 而第一页此刻是满的, 于是下一次写入必然要面对
  325. // "整理本页还是开新页"的抉择。
  326. if err := tbl.DeleteRowByPK(uint64(fill)); err != nil {
  327. t.Fatalf("%s: 删除溢出页里的 pkID=%d: %v", name, fill, err)
  328. }
  329. if got := cmpDataPages(t, tbl, meta); len(got) != 1 || got[0] != firstDP {
  330. t.Fatalf("%s: 前置条件: 溢出页应被回收, 实际页集合 %v (第一页 %d)", name, got, firstDP)
  331. }
  332. // 3) 第一页只留前 keep 行, 其余全删 (墓碑留在第一页)
  333. for pk := uint64(keep + 1); pk <= uint64(firstRows); pk++ {
  334. if err := tbl.DeleteRowByPK(pk); err != nil {
  335. t.Fatalf("%s: 删除 pkID=%d: %v", name, pk, err)
  336. }
  337. }
  338. if dp := cmpLoad(t, tbl, firstDP); dp.DeadBytes() == 0 {
  339. t.Fatalf("%s: 前置条件: 删除后第一页应留下墓碑", name)
  340. }
  341. // 4) 探针: 第一页已满且几乎全是墓碑, 引擎必须在这里抉择
  342. probePK := cmpInsert(t, tbl, 1)[0]
  343. probePage, _, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(probePK), tbl.handle)
  344. if !ok {
  345. t.Fatalf("%s: 探针行 pkID=%d 写入后查不到", name, probePK)
  346. }
  347. out := outcome{firstDP: firstDP, probePage: probePage, pages: len(cmpDataPages(t, tbl, meta))}
  348. mid := cmpLoad(t, tbl, firstDP)
  349. out.firstRows, out.firstDead = mid.Count(), mid.DeadBytes()
  350. // 5) 第一页还能吃下多少行 (探针留在第一页才有意义)
  351. if out.probePage == firstDP {
  352. base := out.pages
  353. for out.extra < 1000 {
  354. cmpInsert(t, tbl, 1)
  355. out.extra++
  356. if len(cmpDataPages(t, tbl, meta)) > base {
  357. break
  358. }
  359. }
  360. }
  361. // 活行必须始终读得到: 整理保持槽位号不变, 叶条目不该指错行
  362. for pk := uint64(1); pk <= keep; pk++ {
  363. if _, err := tbl.FindByPK(pk); err != nil {
  364. t.Errorf("%s: 整理后 pkID=%d 读不回: %v", name, pk, err)
  365. }
  366. }
  367. return out
  368. }
  369. on := run("CMPON", 0.4) // 默认配置: 开页前先按阈值整理
  370. off := run("CMPOFF", 0) // 阈值 <=0: 自动整理关闭
  371. if off.probePage == off.firstDP {
  372. t.Error("关掉自动整理时探针行不该写进那个满是墓碑的页")
  373. }
  374. if off.pages != 2 {
  375. t.Errorf("关掉自动整理时总页数应为 2, 实际 %d", off.pages)
  376. }
  377. if off.firstDead == 0 {
  378. t.Errorf("关掉自动整理后第一页应仍拖着墓碑, 实际 0 B")
  379. }
  380. if on.probePage != on.firstDP {
  381. t.Errorf("自动整理后探针行应写进第一页 %d, 实际落在 %d", on.firstDP, on.probePage)
  382. }
  383. if on.pages != 1 {
  384. t.Errorf("自动整理后不该多开页, 实际 %d 页", on.pages)
  385. }
  386. if on.firstRows != keep+1 {
  387. t.Errorf("自动整理后第一页应只剩 %d 行活行 + 探针行, 实际 %d 行", keep, on.firstRows)
  388. }
  389. if on.firstDead != 0 {
  390. t.Errorf("自动整理后第一页墓碑应清零, 实际 %d B", on.firstDead)
  391. }
  392. // 这才是整理的意义: 同一页随后还能装回大量行 (墓碑占的位置没有白占)
  393. if on.extra < 300 {
  394. t.Errorf("整理后第一页应还能追加 300+ 行, 实际 %d 行", on.extra)
  395. }
  396. t.Logf("写满一页后删剩 %d 行再写 1 行: 自动整理 → 第一页 %d 行/墓碑 %d B, 之后还能追加 %d 行;"+
  397. " 关闭整理 → 第一页 %d 行/墓碑 %d B",
  398. keep, on.firstRows, on.firstDead, on.extra, off.firstRows, off.firstDead)
  399. }
  400. // TestCompactForInsertGuards 覆盖事实 2 的前置判断: 只有"墓碑占比达阈值"且"整理后确实
  401. // 装得下"才值得搬一遍 (整理是 O(页内行数) 的排序 + 拷贝)。两个条件缺一, 都必须在改动
  402. // 本页之前就返回 false —— 调用方据此开新页。
  403. func TestCompactForInsertGuards(t *testing.T) {
  404. cmpFreezeThreshold(t, 0.4)
  405. db := fixOpenDB(t)
  406. tbl := cmpNewTable(t, db, "CMPGUARD")
  407. const rowLen = 24 // 每行 24B, 页内占 4+24=28B
  408. need := uint32(rowLen + 4)
  409. row := func(fill byte) []byte {
  410. r := make([]byte, rowLen)
  411. for i := range r {
  412. r[i] = fill
  413. }
  414. return r
  415. }
  416. // ① 没有墓碑可回收: 不整理
  417. fresh := datapage.New(tbl.handle)
  418. for i := 1; i <= 3; i++ {
  419. fresh.InsertRowSorted(row(byte(i)))
  420. }
  421. fresh.Save()
  422. if fresh.CompactForInsert(need) {
  423. t.Error("没有墓碑时不该整理")
  424. }
  425. // ② 有墓碑, 但"整理了也装不下": 宁可让调用方开新页, 也不白搬一趟
  426. guarded := datapage.New(tbl.handle)
  427. for i := 1; i <= 10; i++ {
  428. guarded.InsertRowSorted(row(byte(i)))
  429. }
  430. for i := 3; i <= 10; i++ { // 删掉 8 行 → 墓碑占比 0.8
  431. if !guarded.DeleteBySlot(uint32(i)) {
  432. t.Fatalf("删除槽位 %d 失败", i)
  433. }
  434. }
  435. guarded.Save()
  436. dead, rows := guarded.DeadBytes(), guarded.Count()
  437. if dead == 0 {
  438. t.Fatal("前置条件: 应留下墓碑")
  439. }
  440. if guarded.CompactForInsert(datapage.PageSize) {
  441. t.Error("整理后也装不下时不该整理")
  442. }
  443. if re := cmpLoad(t, tbl, guarded.PageID()); re.DeadBytes() != dead || re.Count() != rows {
  444. t.Errorf("判断不成立时不该改动本页: 墓碑 %d→%d, 物理行数 %d→%d",
  445. dead, re.DeadBytes(), rows, re.Count())
  446. }
  447. // ③ 墓碑占比没到阈值: 配置说了算
  448. datapage.SetCompactDeadRatio(0.9)
  449. if guarded.CompactForInsert(need) {
  450. t.Error("墓碑占比 0.8 未达阈值 0.9, 不该整理")
  451. }
  452. if re := cmpLoad(t, tbl, guarded.PageID()); re.DeadBytes() != dead {
  453. t.Errorf("未达阈值时不该改动本页: 墓碑 %d→%d", dead, re.DeadBytes())
  454. }
  455. // ④ 阈值达标且确实装得下: 就地整理并落盘, 活行槽位号不变 (1/2 仍在原槽)
  456. datapage.SetCompactDeadRatio(0.4)
  457. if !guarded.CompactForInsert(need) {
  458. t.Fatal("墓碑占比 0.8 已过阈值且整理后装得下, 应当整理")
  459. }
  460. re := cmpLoad(t, tbl, guarded.PageID())
  461. if re.DeadBytes() != 0 || re.Count() != 2 {
  462. t.Errorf("整理后应只剩 2 行活行且无墓碑, 实际 行数=%d 墓碑=%d B", re.Count(), re.DeadBytes())
  463. }
  464. for i := uint32(1); i <= 2; i++ {
  465. if _, ok := re.ReadRowBySlot(i); !ok {
  466. t.Errorf("活行应留在原槽位 %d (槽位号不变是叶条目有效的前提)", i)
  467. }
  468. }
  469. for i := uint32(3); i <= 10; i++ {
  470. if _, ok := re.ReadRowBySlot(i); ok {
  471. t.Errorf("槽位 %d 上的墓碑已被回收, 不该还能读到行", i)
  472. }
  473. }
  474. }
  475. // TestTimeDataPageCompactsBeforeOpeningNewSegment 覆盖事实 2 的时序侧:
  476. // 时序表的"页"就是时间段分段 (扁平索引链 + 每段一个 DataPage), 所以"本段装不下时先按配置
  477. // 整理本段, 整理了也装不下才新开分段"的行为与主键表一致 —— 少开的分段就是少一页碎片。
  478. //
  479. // 构造手法: 每行约 6.2KB (3 个 2048 字节的字符串列), 一页只放得下极少几行 —— 把末段填到
  480. // "再写一行就装不下" (用 IsFull 判定, 不猜容量), 再删掉刚写进去的那一行, 就得到"末段已满
  481. // (行区不因删除而缩小) 且拖着墓碑"的状态, 下一次写入必然要做抉择。
  482. // 用小行构造不出来: 小行的末段永远是刚新开的那个空段, 轮不到整理。
  483. func TestTimeDataPageCompactsBeforeOpeningNewSegment(t *testing.T) {
  484. db := fixOpenDB(t)
  485. const bigLen = 2048 // 3 × 2048 字节字符串 + 时间戳 ⇒ 整行约 6.2KB, 一页两行
  486. big := strings.Repeat("x", bigLen)
  487. newTimeTable := func(name string) (*Table, *IndexMeta, *tsindex.TimeIndexHandler) {
  488. t.Helper()
  489. tbl, err := db.CreateTable(name, []types.FieldInfo{
  490. {FieldType: types.FieldType_Uint64, FieldName: []byte("_ts")},
  491. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(bigLen), FieldName: []byte("s1")},
  492. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(bigLen), FieldName: []byte("s2")},
  493. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(bigLen), FieldName: []byte("s3")},
  494. })
  495. if err != nil {
  496. t.Fatalf("建时序表 %s: %v", name, err)
  497. }
  498. if _, err := tbl.CreateIndex("ts", page.IndexTypeTime, []byte{0}); err != nil {
  499. t.Fatalf("%s: 建时序索引: %v", name, err)
  500. }
  501. return tbl, cmpMetaOf(t, tbl, page.IndexTypeTime), &tsindex.TimeIndexHandler{}
  502. }
  503. type outcome struct {
  504. segments int // 探针写入后的分段数
  505. liveBeforeProbe int // 删除后、探针写入前末段里活着的行数
  506. lastRows uint32 // 末段物理行数
  507. lastDead uint32 // 末段墓碑字节
  508. }
  509. run := func(name string, ratio float64) outcome {
  510. cmpFreezeThreshold(t, ratio)
  511. tbl, meta, h := newTimeTable(name)
  512. // 宽行在页内的占用: storeRow = [pkID 8B][rowData], rowData = [版本(1) + 列偏移表(2×3) +
  513. // 时间戳(8) + 3×2048 字符串 + NULL 位图(1)]。
  514. // 注意 IsFull 的 rowSize 约定【不含】4B 长度前缀 (见 datapage.IsFull 注释), 别多算。
  515. rowSize := uint32(8 + (1 + 2*3 + 8 + 3*bigLen + 1))
  516. ts := uint64(1_700_000_000_000) // 毫秒时间戳, 必须非递减 (时序表只收顺序写入)
  517. insert := func() uint64 {
  518. t.Helper()
  519. pk := tbl.AllocPKID()
  520. if _, err := tbl.InsertRowWithID(pk, []types.FieldValue{
  521. {FieldName: []byte("_ts"), Value: ts},
  522. {FieldName: []byte("s1"), Value: big},
  523. {FieldName: []byte("s2"), Value: big},
  524. {FieldName: []byte("s3"), Value: big},
  525. }); err != nil {
  526. t.Fatalf("%s: 插入 ts=%d: %v", name, ts, err)
  527. }
  528. ts += 1000
  529. return pk
  530. }
  531. // 1) 先插到出现第 2 个分段: 第 1 段至此装满, 末段 (seg2) 里有且只有刚溢出的那一行
  532. pk1, pk2 := insert(), insert()
  533. lastPK := uint64(0)
  534. for len(h.GetDataPageIDs(tbl.handle, meta.EntryPage)) < 2 {
  535. lastPK = insert()
  536. }
  537. seg2 := h.GetDataPageIDs(tbl.handle, meta.EntryPage)[1]
  538. // 2) 把末段填到"再插一行同样的宽行就装不下"为止 —— 用 IsFull 判定, 不猜页容量
  539. for !cmpLoad(t, tbl, seg2).IsFull(rowSize) {
  540. lastPK = insert()
  541. }
  542. // 3) 删掉刚写进末段的那一行: 删除不缩小行区, 所以末段对本尺寸的行仍然"满",
  543. // 但多了一块墓碑 —— 这正是"该整理"的状态。
  544. // (时序删除有意不回收页与分段条目: 条目还指向它, 只是行没了。)
  545. if err := tbl.DeleteRowByPK(lastPK); err != nil {
  546. t.Fatalf("%s: 删除 pkID=%d: %v", name, lastPK, err)
  547. }
  548. before := cmpLoad(t, tbl, seg2)
  549. if before.DeadBytes() == 0 {
  550. t.Fatalf("%s: 前置条件: 末段删除后应留下墓碑", name)
  551. }
  552. if !before.IsFull(rowSize) {
  553. t.Fatalf("%s: 前置条件: 删掉一行后末段对宽行仍应是满的", name)
  554. }
  555. if got := h.GetDataPageIDs(tbl.handle, meta.EntryPage); len(got) != 2 || got[1] != seg2 {
  556. t.Fatalf("%s: 前置条件: 分段应仍是 2 个 (末段 %d), 实际 %v", name, seg2, got)
  557. }
  558. // 末段里还活着的行数 (删掉 lastPK 之后的幸存者) —— 整理只回收墓碑, 这些行必须原地留下,
  559. // 所以"整理成功"之后的末段行数必然等于它 + 1 (探针那一行)。不猜页容量。
  560. liveBefore := 0
  561. for s := uint32(1); s <= before.SlotCount(); s++ {
  562. if _, ok := before.ReadRowBySlot(s); ok {
  563. liveBefore++
  564. }
  565. }
  566. // 4) 探针: 时间戳更大 → 路由到末段 (它满且拖着墓碑) → 引擎在这里抉择
  567. probePK := insert()
  568. out := outcome{segments: len(h.GetDataPageIDs(tbl.handle, meta.EntryPage)), liveBeforeProbe: liveBefore}
  569. last := cmpLoad(t, tbl, seg2)
  570. out.lastRows, out.lastDead = last.Count(), last.DeadBytes()
  571. // 分段里的活行必须一直读得到 (整理保持槽位号不变, 索引条目不该指错行)
  572. for _, pk := range []uint64{pk1, pk2, probePK} {
  573. if _, err := tbl.FindByPK(pk); err != nil {
  574. t.Errorf("%s: pkID=%d 读不回: %v", name, pk, err)
  575. }
  576. }
  577. return out
  578. }
  579. on := run("TSPON", 0.4) // 默认配置: 开新分段前先按阈值整理本段
  580. off := run("TSPOFF", 0) // 阈值 <=0: 自动整理关闭
  581. if off.segments != 3 {
  582. t.Errorf("关掉自动整理时探针行应开新分段 (共 3 段), 实际 %d 段", off.segments)
  583. }
  584. if off.lastDead == 0 {
  585. t.Errorf("关掉自动整理后末段应仍拖着墓碑, 实际 0 B")
  586. }
  587. if on.segments != 2 {
  588. t.Errorf("自动整理后探针行应写进本段 (分段数不变 = 2), 实际 %d 段", on.segments)
  589. }
  590. if want := uint32(on.liveBeforeProbe + 1); on.lastRows != want {
  591. t.Errorf("自动整理后末段应只剩 %d 行 (整理前 %d 活行 + 探针 1 行), 实际 %d 行",
  592. want, on.liveBeforeProbe, on.lastRows)
  593. }
  594. if on.lastDead != 0 {
  595. t.Errorf("自动整理后末段墓碑应清零, 实际 %d B", on.lastDead)
  596. }
  597. t.Logf("时序表末段写满后删一行再写 1 行: 自动整理 → 分段数 %d, 末段 %d 行/墓碑 %d B;"+
  598. " 关闭整理 → 分段数 %d, 末段 %d 行/墓碑 %d B",
  599. on.segments, on.lastRows, on.lastDead, off.segments, off.lastRows, off.lastDead)
  600. }
  601. // TestDataPageUpdateInPlaceVsRebuild 钉死事实 3:
  602. // 等长更新原地覆盖 —— 不产生墓碑, 也就没有"整理"的需求;
  603. // 变长更新装不下时走页内重建 —— 它会顺带回收墓碑, 但触发条件是"装不下"而非阈值。
  604. func TestDataPageUpdateInPlaceVsRebuild(t *testing.T) {
  605. cmpFreezeThreshold(t, 0.4)
  606. db := fixOpenDB(t)
  607. tbl := cmpNewVarTable(t, db, "CMPUPD")
  608. meta := cmpPrimaryMeta(t, tbl)
  609. const total, live = 40, 25
  610. pks := make([]uint64, 0, total)
  611. for i := 0; i < total; i++ {
  612. pk := tbl.AllocPKID()
  613. if _, err := tbl.InsertRowWithID(pk, []types.FieldValue{
  614. {FieldName: []byte("id"), Value: pk},
  615. {FieldName: []byte("s"), Value: "abc"},
  616. }); err != nil {
  617. t.Fatalf("插入第 %d 行: %v", i, err)
  618. }
  619. pks = append(pks, pk)
  620. }
  621. dpID := cmpOnlyPage(t, tbl, meta)
  622. for _, pk := range pks[live:] {
  623. if err := tbl.DeleteRowByPK(pk); err != nil {
  624. t.Fatalf("删除 pkID=%d: %v", pk, err)
  625. }
  626. }
  627. // 记下活行槽位: 变长更新若换了槽位/换了页, 这里会立刻发现
  628. beforeSlot := make(map[uint64]uint16, live)
  629. for _, pk := range pks[:live] {
  630. _, slot, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(pk), tbl.handle)
  631. if !ok {
  632. t.Fatalf("前置条件: pkID=%d 应能定位", pk)
  633. }
  634. beforeSlot[pk] = slot
  635. }
  636. deletedBytes := cmpLoad(t, tbl, dpID).DeadBytes()
  637. if deletedBytes == 0 {
  638. t.Fatal("前置条件: 删除后应留下墓碑字节")
  639. }
  640. // (1) 等长更新: "abc" → "xyz", 行长不变 → UpdateBySlot 原地覆盖。
  641. // 原地覆盖既不产生新墓碑, 也不回收旧墓碑 —— 失效字节保持不动。
  642. if err := tbl.UpdateRowByPK(pks[0], []types.FieldValue{
  643. {FieldName: []byte("s"), Value: "xyz"},
  644. }); err != nil {
  645. t.Fatalf("等长更新应当成功: %v", err)
  646. }
  647. dp := cmpLoad(t, tbl, dpID)
  648. if dp.DeadBytes() != deletedBytes {
  649. t.Errorf("等长更新不该动墓碑: 更新前 %d, 更新后 %d", deletedBytes, dp.DeadBytes())
  650. }
  651. if dp.Count() != uint32(total) {
  652. t.Errorf("等长更新不该重排页: 物理行数应为 %d, 实际 %d", total, dp.Count())
  653. }
  654. if got, err := tbl.FindByPK(pks[0]); err != nil {
  655. t.Errorf("等长更新后读不回: %v", err)
  656. } else if cmpStr(t, got[len(got)-1]) != "xyz" {
  657. t.Errorf("等长更新没生效: %v", got)
  658. }
  659. // (2) 变长更新: 3 字节 → 40 字节, 原地装不下 → 页内重建 (RebuildWithSlot)。
  660. // 重建会紧凑重排, 于是墓碑被【顺带】回收 —— 但它不是被阈值触发的。
  661. long := ""
  662. for i := 0; i < 40; i++ {
  663. long += "x"
  664. }
  665. if err := tbl.UpdateRowByPK(pks[1], []types.FieldValue{
  666. {FieldName: []byte("s"), Value: long},
  667. }); err != nil {
  668. t.Fatalf("变长更新应当成功 (页内重建): %v", err)
  669. }
  670. dp = cmpLoad(t, tbl, dpID)
  671. if dp.DeadBytes() != 0 {
  672. t.Errorf("页内重建应顺带回收墓碑, 实际仍剩 %d 字节", dp.DeadBytes())
  673. }
  674. if dp.Count() != uint32(live) {
  675. t.Errorf("页内重建后物理行数应等于活行数 %d, 实际 %d", live, dp.Count())
  676. }
  677. if dpIDs := cmpDataPages(t, tbl, meta); len(dpIDs) != 1 || dpIDs[0] != dpID {
  678. t.Errorf("变长 40 字节应当仍在本页, 页集合变成 %v", dpIDs)
  679. }
  680. for _, pk := range pks[:live] {
  681. _, slot, ok := btree.BTreePKSearch(meta.EntryPage, cmpBE(pk), tbl.handle)
  682. if !ok || slot != beforeSlot[pk] {
  683. t.Errorf("pkID=%d 的槽位从 %d 变成 %d (ok=%v) —— 叶条目会指错行",
  684. pk, beforeSlot[pk], slot, ok)
  685. }
  686. }
  687. if got, err := tbl.FindByPK(pks[1]); err != nil {
  688. t.Errorf("变长更新后读不回: %v", err)
  689. } else if cmpStr(t, got[len(got)-1]) != long {
  690. t.Errorf("变长更新没生效: %v", got)
  691. }
  692. }