vacuum_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. package core
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "sync"
  7. "testing"
  8. "time"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  11. )
  12. // vacNewTable 建一个普通主键表 (uint64 id + uint64 v)
  13. func vacNewTable(t *testing.T, db *DB, name string) *Table {
  14. t.Helper()
  15. tbl, err := db.CreateTable(name, []types.FieldInfo{
  16. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  17. {FieldType: types.FieldType_Uint64, FieldName: []byte("v")},
  18. })
  19. if err != nil {
  20. t.Fatalf("建表 %s: %v", name, err)
  21. }
  22. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  23. t.Fatalf("建主键索引: %v", err)
  24. }
  25. return tbl
  26. }
  27. // vacInsert 插入 n 行 (id=i+1, v=i*10)
  28. func vacInsert(t *testing.T, tbl *Table, n int) {
  29. t.Helper()
  30. for i := 0; i < n; i++ {
  31. if _, err := tbl.InsertRowWithID(uint64(i+1), []types.FieldValue{
  32. {FieldName: []byte("id"), Value: uint64(i + 1)},
  33. {FieldName: []byte("v"), Value: uint64(i * 10)},
  34. }); err != nil {
  35. t.Fatalf("插入第 %d 行: %v", i+1, err)
  36. }
  37. }
  38. }
  39. // vacCount 拿表的当前行数
  40. func vacCount(t *testing.T, tbl *Table) int {
  41. t.Helper()
  42. c, err := tbl.Query().Count()
  43. if err != nil {
  44. t.Fatalf("Count: %v", err)
  45. }
  46. return int(c)
  47. }
  48. // ============================================================================
  49. // 测试用例
  50. // ============================================================================
  51. // TestVACUUMEmpty 空库跑 VACUUM 不应崩溃
  52. func TestVACUUMEmpty(t *testing.T) {
  53. db := fixOpenDB(t)
  54. res, err := db.VACUUM()
  55. if err != nil {
  56. t.Fatalf("空库 VACUUM 失败: %v", err)
  57. }
  58. t.Logf("空库 VACUUM 结果: tables=%d rows=%d dur=%v", res.Tables, res.RowsCopied, res.Duration)
  59. if res.Tables != 0 {
  60. t.Errorf("空库应 tables=0, 得到 %d", res.Tables)
  61. }
  62. }
  63. // TestVACUUMKeepData 插 1000 行, 跑 VACUUM, 行数与数据应一致
  64. func TestVACUUMKeepData(t *testing.T) {
  65. dir := t.TempDir()
  66. path := filepath.Join(dir, "test.db")
  67. db, err := Open(path)
  68. if err != nil {
  69. t.Fatalf("open: %v", err)
  70. }
  71. db.SetAutoCommitDelay(0)
  72. t.Cleanup(func() { _ = db.Close() })
  73. tbl := vacNewTable(t, db, "t")
  74. vacInsert(t, tbl, 1000)
  75. if err := db.Commit(); err != nil {
  76. t.Fatalf("commit: %v", err)
  77. }
  78. oldSize, _ := os.Stat(path)
  79. res, err := db.VACUUM(VACUUMOptions{
  80. Progress: func(phase string, copied, total int64) {
  81. if copied%100 == 0 || phase == "done" || phase == "scan" || phase == "swap" {
  82. t.Logf(" progress: phase=%s copied=%d total=%d", phase, copied, total)
  83. }
  84. },
  85. })
  86. if err != nil {
  87. t.Fatalf("VACUUM 失败: %v", err)
  88. }
  89. t.Logf("整理结果: 旧=%d 新=%d 节省=%d (%.1f%%) tables=%d rows=%d 耗时=%v",
  90. res.OldFileSize, res.NewFileSize, res.SavedBytes, res.SavedPercent,
  91. res.Tables, res.RowsCopied, res.Duration)
  92. if res.RowsCopied != 1000 {
  93. t.Errorf("应复制 1000 行, 得到 %d", res.RowsCopied)
  94. }
  95. if res.Tables != 1 {
  96. t.Errorf("应有 1 张表, 得到 %d", res.Tables)
  97. }
  98. if res.NewFileSize == 0 {
  99. t.Errorf("新文件大小不应为 0")
  100. }
  101. if oldSize != nil && res.OldFileSize != oldSize.Size() {
  102. t.Errorf("旧文件大小不匹配: 期望 %d, 得到 %d", oldSize.Size(), res.OldFileSize)
  103. }
  104. c := vacCount(t, tbl)
  105. if c != 1000 {
  106. t.Errorf("VACUUM 后行数应仍为 1000, 得到 %d", c)
  107. }
  108. // 抽查几行的值
  109. for _, id := range []uint64{1, 100, 500, 999, 1000} {
  110. row, err := tbl.FindByPK(id)
  111. if err != nil {
  112. t.Errorf("查 id=%d 失败: %v", id, err)
  113. continue
  114. }
  115. expectedV := uint64(id-1) * 10
  116. if len(row) >= 2 {
  117. if v, ok := row[1].Value.(uint64); !ok || v != expectedV {
  118. t.Errorf("id=%d 的 v 应为 %d, 得到 %v", id, expectedV, row[1])
  119. }
  120. }
  121. }
  122. }
  123. // TestVACUUMShrinkFile 删一半数据后 VACUUM, 文件应显著缩小
  124. func TestVACUUMShrinkFile(t *testing.T) {
  125. dir := t.TempDir()
  126. path := filepath.Join(dir, "test.db")
  127. db, err := Open(path)
  128. if err != nil {
  129. t.Fatalf("open: %v", err)
  130. }
  131. db.SetAutoCommitDelay(0)
  132. t.Cleanup(func() { _ = db.Close() })
  133. tbl := vacNewTable(t, db, "t")
  134. vacInsert(t, tbl, 5000)
  135. if err := db.Commit(); err != nil {
  136. t.Fatalf("commit: %v", err)
  137. }
  138. oldSize, _ := os.Stat(path)
  139. if oldSize == nil {
  140. t.Fatal("拿不到旧大小")
  141. }
  142. t.Logf("插完: %d 字节", oldSize.Size())
  143. for id := uint64(1); id <= 2500; id++ {
  144. if err := tbl.DeleteRowByPK(id); err != nil {
  145. t.Fatalf("删 id=%d 失败: %v", id, err)
  146. }
  147. }
  148. if err := db.Commit(); err != nil {
  149. t.Fatalf("commit: %v", err)
  150. }
  151. afterDelete, _ := os.Stat(path)
  152. t.Logf("删一半后: %d 字节 (只有墓碑, 不明显缩)", afterDelete.Size())
  153. res, err := db.VACUUM()
  154. if err != nil {
  155. t.Fatalf("VACUUM 失败: %v", err)
  156. }
  157. t.Logf("VACUUM 后: %d 字节 (节省 %d, %.1f%%) 耗时=%v",
  158. res.NewFileSize, res.SavedBytes, res.SavedPercent, res.Duration)
  159. if res.NewFileSize >= afterDelete.Size() {
  160. t.Logf("注意: VACUUM 后文件未明显缩小 (旧=%d 新=%d) — 小库可能因最小页数未减少",
  161. afterDelete.Size(), res.NewFileSize)
  162. }
  163. c := vacCount(t, tbl)
  164. if c != 2500 {
  165. t.Errorf("VACUUM 后应剩 2500 行, 得到 %d", c)
  166. }
  167. }
  168. // TestVACUUMMultiTable 多表多索引 VACUUM
  169. func TestVACUUMMultiTable(t *testing.T) {
  170. dir := t.TempDir()
  171. path := filepath.Join(dir, "test.db")
  172. db, err := Open(path)
  173. if err != nil {
  174. t.Fatalf("open: %v", err)
  175. }
  176. db.SetAutoCommitDelay(0)
  177. t.Cleanup(func() { _ = db.Close() })
  178. expectedRows := 0
  179. for i, name := range []string{"a", "b", "c"} {
  180. tbl := vacNewTable(t, db, name)
  181. n := 100 * (i + 1)
  182. vacInsert(t, tbl, n)
  183. expectedRows += n
  184. if name == "a" {
  185. // 表 a 加二级 B+ 树索引 (v 列, 唯一)
  186. if _, err := tbl.CreateIndex("v_idx", page.IndexTypeBTree, []byte{1}, true); err != nil {
  187. t.Fatalf("建二级索引: %v", err)
  188. }
  189. }
  190. }
  191. if err := db.Commit(); err != nil {
  192. t.Fatalf("commit: %v", err)
  193. }
  194. res, err := db.VACUUM()
  195. if err != nil {
  196. t.Fatalf("VACUUM 失败: %v", err)
  197. }
  198. t.Logf("MultiTable VACUUM: copied=%d tables=%d old=%d new=%d", res.RowsCopied, res.Tables, res.OldFileSize, res.NewFileSize)
  199. if res.Tables != 3 {
  200. t.Errorf("应有 3 张表, 得到 %d", res.Tables)
  201. }
  202. if res.RowsCopied != int64(expectedRows) {
  203. t.Errorf("应复制 %d 行, 得到 %d", expectedRows, res.RowsCopied)
  204. }
  205. for i, name := range []string{"a", "b", "c"} {
  206. tbl, ok := db.FindTable(name)
  207. if !ok {
  208. t.Errorf("表 %s 不见了", name)
  209. continue
  210. }
  211. c := vacCount(t, tbl)
  212. // 直接读主键 DataPage 的 LiveCount
  213. primMeta, ok := tbl.IndexMeta().GetRowStoreIndex()
  214. t.Logf(" 表 %s: Count()=%d TotalRows=%d primMeta=%+v ok=%v",
  215. name, c, tbl.TotalRows(), primMeta, ok)
  216. if c != 100*(i+1) {
  217. t.Errorf("表 %s 应有 %d 行, 得到 %d", name, 100*(i+1), c)
  218. }
  219. }
  220. // 验证表 a 的二级索引: v=990 对应 id=100 (因为 v=(id-1)*10, 即 v=99*10=990)
  221. tblA, _ := db.FindTable("a")
  222. row, err := tblA.SearchByIndex("v_idx", encodeUint64BE(990))
  223. if err != nil {
  224. t.Errorf("通过二级索引查 v=990 失败: %v", err)
  225. }
  226. if len(row) >= 1 {
  227. if id, ok := row[0].Value.(uint64); !ok || id != 100 {
  228. t.Errorf("通过 v=990 期望 id=100, 得到 %v", row[0])
  229. }
  230. }
  231. }
  232. // TestVACUUMRejectTxn 活跃事务中调用 VACUUM 应被拒绝
  233. func TestVACUUMRejectTxn(t *testing.T) {
  234. dir := t.TempDir()
  235. path := filepath.Join(dir, "test.db")
  236. db, err := Open(path)
  237. if err != nil {
  238. t.Fatalf("open: %v", err)
  239. }
  240. db.SetAutoCommitDelay(0)
  241. t.Cleanup(func() { _ = db.Close() })
  242. tbl := vacNewTable(t, db, "t")
  243. vacInsert(t, tbl, 10)
  244. _ = db.BeginTransaction()
  245. _, err = db.VACUUM()
  246. if err == nil {
  247. t.Error("活跃事务中 VACUUM 应报错, 但通过了")
  248. } else {
  249. t.Logf("正确拒绝: %v", err)
  250. }
  251. }
  252. // TestVACUUMRejectNoPath 对 /dev/null 库的 VACUUM 应被拒绝
  253. func TestVACUUMRejectNoPath(t *testing.T) {
  254. // 这个测试用例跳过 — 我们没有合适的"无路径" db 创建方式
  255. // 实际生产中, Open 时会强制要求路径
  256. t.Skip("跳过: 无路径 db 创建路径")
  257. }
  258. // TestAutoVACUUM 自动调度: 间隔触发
  259. func TestAutoVACUUM(t *testing.T) {
  260. dir := t.TempDir()
  261. path := filepath.Join(dir, "test.db")
  262. db, err := Open(path)
  263. if err != nil {
  264. t.Fatalf("open: %v", err)
  265. }
  266. db.SetAutoCommitDelay(0)
  267. t.Cleanup(func() {
  268. db.SetAutoVACUUM(0)
  269. _ = db.Close()
  270. })
  271. tbl := vacNewTable(t, db, "t")
  272. vacInsert(t, tbl, 50)
  273. if err := db.Commit(); err != nil {
  274. t.Fatalf("commit: %v", err)
  275. }
  276. // 启用自动 VACUUM: 间隔 50ms (测试用短间隔)
  277. db.SetAutoVACUUM(50 * time.Millisecond)
  278. // 轮询等首次自动 VACUUM 完成 (固定 sleep 在 -race 下必然脆弱)
  279. before := db.LastVACUUM()
  280. deadline := time.Now().Add(2 * time.Second)
  281. for time.Now().Before(deadline) && !db.LastVACUUM().After(before) {
  282. time.Sleep(10 * time.Millisecond)
  283. }
  284. // 期间加点数据。自动 VACUUM 可能已换过引擎状态, 必须用【当前句柄】取表。
  285. tbl2, ok := db.FindTable("t")
  286. if !ok {
  287. t.Fatalf("自动 VACUUM 后表丢失")
  288. }
  289. for i := 100; i < 110; i++ {
  290. if _, err := tbl2.InsertRowWithID(uint64(i), []types.FieldValue{
  291. {FieldName: []byte("id"), Value: uint64(i)},
  292. {FieldName: []byte("v"), Value: uint64(i * 10)},
  293. }); err != nil {
  294. t.Fatalf("插入 %d: %v", i, err)
  295. }
  296. }
  297. _ = db.Commit()
  298. // 关闭自动 VACUUM
  299. db.SetAutoVACUUM(0)
  300. // 验证表还在 (初始 50 行 + 新插 10 行; 用当前句柄取表再数)
  301. tbl3, ok := db.FindTable("t")
  302. if !ok {
  303. t.Fatalf("取表失败")
  304. }
  305. cnt, _ := tbl3.Query().Count()
  306. if cnt < 50 {
  307. t.Errorf("行数应不少于 50, 得到 %d", cnt)
  308. }
  309. t.Logf("LastVACUUM: %v (当前: %v)", db.LastVACUUM(), time.Now())
  310. }
  311. // TestAutoVACUUMRepeats 回归测试: 自动 VACUUM 必须周期重排, 而不是首轮之后静默停摆。
  312. //
  313. // 旧实现 VACUUM 内部 `*db = *newDB2` 整体结构体拷贝, 把 autoVacuumEnabled 覆盖成
  314. // false —— 首轮跑完后调度器读到 stillEnabled=false 不再重排定时器, 自动 VACUUM
  315. // 实际只生效一次 (且该整体拷贝因复制互斥锁被 go vet copylocks 报警)。
  316. func TestAutoVACUUMRepeats(t *testing.T) {
  317. dir := t.TempDir()
  318. path := filepath.Join(dir, "test.db")
  319. db, err := Open(path)
  320. if err != nil {
  321. t.Fatalf("open: %v", err)
  322. }
  323. db.SetAutoCommitDelay(0)
  324. t.Cleanup(func() {
  325. db.SetAutoVACUUM(0)
  326. _ = db.Close()
  327. })
  328. tbl := vacNewTable(t, db, "t")
  329. vacInsert(t, tbl, 20)
  330. if err := db.Commit(); err != nil {
  331. t.Fatalf("commit: %v", err)
  332. }
  333. var mu sync.Mutex
  334. dones := 0
  335. db.SetAutoVACUUM(40*time.Millisecond, VACUUMOptions{
  336. Progress: func(phase string, _, _ int64) {
  337. if phase == "done" {
  338. mu.Lock()
  339. dones++
  340. mu.Unlock()
  341. }
  342. },
  343. })
  344. deadline := time.Now().Add(600 * time.Millisecond)
  345. for time.Now().Before(deadline) {
  346. time.Sleep(20 * time.Millisecond)
  347. mu.Lock()
  348. n := dones
  349. mu.Unlock()
  350. if n >= 2 {
  351. break
  352. }
  353. }
  354. db.SetAutoVACUUM(0)
  355. mu.Lock()
  356. defer mu.Unlock()
  357. if dones < 2 {
  358. t.Fatalf("自动 VACUUM 应周期重排, 600ms 内只触发了 %d 次", dones)
  359. }
  360. t.Logf("600ms 内自动 VACUUM 触发 %d 次", dones)
  361. }
  362. // TestVACUUMPreservesRuntimeConfig 回归测试: VACUUM 更换引擎状态后,
  363. // 用户配置 (autoCommitDelay) 必须保留, 句柄必须继续可用。
  364. //
  365. // 旧实现整体结构体拷贝会把 autoCommitDelay 重置为默认值 —— 用户设的手动提交模式
  366. // (0 = 禁用防抖) 被静默改回 1s 防抖。
  367. func TestVACUUMPreservesRuntimeConfig(t *testing.T) {
  368. dir := t.TempDir()
  369. path := filepath.Join(dir, "test.db")
  370. db, err := Open(path)
  371. if err != nil {
  372. t.Fatalf("open: %v", err)
  373. }
  374. defer db.Close()
  375. db.SetAutoCommitDelay(0) // 手动提交模式
  376. tbl := vacNewTable(t, db, "t")
  377. vacInsert(t, tbl, 10)
  378. if err := db.Commit(); err != nil {
  379. t.Fatalf("commit: %v", err)
  380. }
  381. if _, err := db.VACUUM(); err != nil {
  382. t.Fatalf("VACUUM: %v", err)
  383. }
  384. // 1) 运行时配置保留: 手动提交模式不被静默改回防抖模式
  385. if db.autoCommitDelay != 0 {
  386. t.Fatalf("VACUUM 后 autoCommitDelay 应保留为 0, 得到 %v", db.autoCommitDelay)
  387. }
  388. // 2) 句柄可用: VACUUM 之后建新表、插入、查询
  389. tbl2 := vacNewTable(t, db, "t2")
  390. for i := 0; i < 5; i++ {
  391. if _, err := tbl2.InsertRowWithID(uint64(i+1), []types.FieldValue{
  392. {FieldName: []byte("id"), Value: uint64(i + 1)},
  393. {FieldName: []byte("v"), Value: uint64(i * 100)},
  394. }); err != nil {
  395. t.Fatalf("VACUUM 后插入第 %d 行: %v", i+1, err)
  396. }
  397. }
  398. if err := db.Commit(); err != nil {
  399. t.Fatalf("VACUUM 后 commit: %v", err)
  400. }
  401. if got := vacCount(t, tbl2); got != 5 {
  402. t.Fatalf("t2 应有 5 行, 得到 %d", got)
  403. }
  404. }
  405. // ============================================================================
  406. // 辅助
  407. // ============================================================================
  408. // encodeUint64BE 编码 uint64 为大端 8 字节 (二级索引查找用)
  409. func encodeUint64BE(v uint64) []byte {
  410. b := make([]byte, 8)
  411. for i := 7; i >= 0; i-- {
  412. b[i] = byte(v)
  413. v >>= 8
  414. }
  415. return b
  416. }
  417. var _ = fmt.Sprintf