main.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package main
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB"
  4. "fmt"
  5. "os"
  6. "time"
  7. )
  8. type Record struct {
  9. DB.BaseModel
  10. Name string `vrtx:"maxlen:32"`
  11. }
  12. const (
  13. dbPath = "lock_table_test.db"
  14. poolPath = "lock_test2.db"
  15. )
  16. // failed 累计失败项。demo 用退出码说话: 只打印一句 "⚠️ 未检测到阻塞" 然后正常退出,
  17. // 等于把"没验到"当成"验过了" —— 这正是老版本测试2/3 的样子。
  18. var failed int
  19. func check(ok bool, format string, args ...any) {
  20. if ok {
  21. fmt.Printf(" ✅ "+format+"\n", args...)
  22. return
  23. }
  24. failed++
  25. fmt.Printf(" ❌ "+format+"\n", args...)
  26. }
  27. func main() {
  28. fmt.Println("==============================================")
  29. fmt.Println(" 锁表机制测试")
  30. fmt.Print("==============================================\n\n")
  31. os.Remove(dbPath)
  32. db, err := DB.Open(dbPath)
  33. if err != nil {
  34. panic(err)
  35. }
  36. tbl, err := DB.UseTable[Record](db, "test")
  37. if err != nil {
  38. panic(err)
  39. }
  40. testManualLock(tbl, db)
  41. testAutoCommitOnThreshold(tbl, db)
  42. testBufferPoolFull()
  43. testReopenDurability(tbl, db)
  44. }
  45. // testManualLock 钉住 README 注意事项 7 的语义: LockWrites 挡住的是【页落盘】,
  46. // 不是 Insert 本身 —— 锁定期插入被阻塞但不报错, 解锁后一条不少地进去。
  47. //
  48. // 老版本这里断言的是"锁定期插入应当被拒绝并返回错误"。那是错的: 引擎里根本没有
  49. // "写入被拒绝"这种错误码, BufferPool.WritePage 走的是阻塞等待 (waitForWriteLock)。
  50. // 于是插入在解锁后正常返回 err == nil, 断言"应为错误"必然成立不了 —— demo 一直是红的,
  51. // 而 README 又写着"全部通过"。
  52. func testManualLock(tbl *DB.Table[Record], db *DB.DB) {
  53. fmt.Println("测试1: 手动 LockWrites/UnlockWrites")
  54. before, err := tbl.Count()
  55. if err != nil {
  56. check(false, "前置查询行数: %v", err)
  57. return
  58. }
  59. db.LockWrites()
  60. fmt.Println(" ✅ 已锁定写入")
  61. type result struct {
  62. err error
  63. elapsed time.Duration
  64. }
  65. rec := &Record{Name: "Emp0000001"}
  66. done := make(chan result, 1)
  67. go func() {
  68. start := time.Now()
  69. insertErr := tbl.Insert(rec)
  70. done <- result{err: insertErr, elapsed: time.Since(start)}
  71. }()
  72. // 用"有没有结果"判断阻塞, 而不是用耗时阈值 —— 阈值只能证明"慢", 证明不了"被挡住"。
  73. var res result
  74. blocked := true
  75. select {
  76. case res = <-done:
  77. blocked = false
  78. case <-time.After(100 * time.Millisecond):
  79. }
  80. check(blocked, "锁定期插入被阻塞在页落盘处")
  81. db.UnlockWrites()
  82. fmt.Println(" ✅ 已解锁写入")
  83. if blocked {
  84. // 被挡住的那一支还没收到结果, 等它跑完再验 —— 否则下面查的是"插入之前"的行数。
  85. res = <-done
  86. }
  87. check(res.err == nil, "锁定期写入不报错 (err=%v, 解锁后 %v 完成)", res.err, res.elapsed)
  88. after, err := tbl.Count()
  89. check(err == nil && after == before+1, "解锁后真的落盘: %d → %d 行", before, after)
  90. if rec.ID != 0 {
  91. got, findErr := tbl.Find(rec.ID)
  92. check(findErr == nil && got != nil, "能按引擎分配到的 _id=%d 读回", rec.ID)
  93. } else {
  94. check(false, "插入后结构体没有回填 _id, 无法验证读回")
  95. }
  96. }
  97. // testAutoCommitOnThreshold 钉住真正的写入路径: 脏页阈值在 WritePage 里【同步】触发
  98. // Commit (pool.WritePage 在锁外直接调 onThreshold), 也就是"写到一半顺手提交一次"。
  99. // SetCommitPreset 的每一档都靠它, 200 行配 5 页阈值必然触发很多次。
  100. //
  101. // 为什么不用"一边写一边在另一个 goroutine 里 Commit": 引擎是【单写入者模型】
  102. // (见 pool/buffer_pool.go 里关于 capacityWait 死锁的注释), 写入与 Commit 必须由同一个
  103. // 写入者先后调用。跨 goroutine 重叠会踩到一个真问题: CollectDirty 把脏页移出缓存之后、
  104. // 回写磁盘之前, 这一页既不在缓存里也还不在盘上, 并发的读会从盘上读到全零页 ——
  105. // 实测稳定 panic (btPID: slice bounds out of range [-4:], 页头 EntryLen 读到 0)。
  106. // 那条路要等引擎补上写入串行化才能测。
  107. func testAutoCommitOnThreshold(tbl *DB.Table[Record], db *DB.DB) {
  108. fmt.Println("\n测试2: 写入过程中脏页阈值触发自动提交")
  109. db.SetAutoCommitDelay(0) // 关掉防抖计时器, 只留阈值触发, 保证提交是同步发生的
  110. db.SetDirtyThreshold(5) // 5 个脏页提交一次, 下面的行数必然触发多次
  111. before, err := tbl.Count()
  112. if err != nil {
  113. check(false, "前置查询行数: %v", err)
  114. return
  115. }
  116. const rows = 200
  117. for i := 0; i < rows; i++ {
  118. if insertErr := tbl.Insert(&Record{Name: fmt.Sprintf("Auto%06d", i)}); insertErr != nil {
  119. check(false, "写入第 %d 行时失败: %v", i+1, insertErr)
  120. return
  121. }
  122. }
  123. after, err := tbl.Count()
  124. check(err == nil && after == before+rows, "穿插 %d 次阈值提交后一行不丢: %d → %d",
  125. rows/5, before, after)
  126. }
  127. // testBufferPoolFull 钉住 README 注意事项 7 的第三条: 池子满时淘汰干净页而不是死锁。
  128. //
  129. // 老版本同样只看耗时 (> 50ms), 既证明不了"淘汰了干净页", 也证明不了"不死锁"—— 真死锁
  130. // 了它只会永远挂住。这里改成两条硬断言: 写入必须在时限内跑完 (超时即失败, 把"挂住"
  131. // 变成"报错"), 并且淘汰之后每一行都还得读得回来 (淘汰逻辑不能吃掉数据)。
  132. func testBufferPoolFull() {
  133. fmt.Println("\n测试3: BufferPool 满时淘汰干净页 (容量 10 页)")
  134. os.Remove(poolPath)
  135. db2, err := DB.Open(poolPath)
  136. if err != nil {
  137. check(false, "打开 %s: %v", poolPath, err)
  138. return
  139. }
  140. defer func() {
  141. db2.Close()
  142. os.Remove(poolPath)
  143. }()
  144. db2.SetBufferPoolCapacity(10) // 只允许 10 页
  145. tbl2, err := DB.UseTable[Record](db2, "test")
  146. if err != nil {
  147. check(false, "建表: %v", err)
  148. return
  149. }
  150. const rows = 200
  151. done := make(chan error, 1)
  152. go func() {
  153. for i := 1; i <= rows; i++ {
  154. if insertErr := tbl2.Insert(&Record{Name: fmt.Sprintf("Emp%07d", i)}); insertErr != nil {
  155. done <- insertErr
  156. return
  157. }
  158. }
  159. done <- nil
  160. }()
  161. // 容量 10 页远小于 200 行占的页数, 插入必然要边走边淘汰; 给它一个上限,
  162. // 拿不到结果就是"没能淘汰干净页把自己憋死了"。
  163. select {
  164. case err := <-done:
  165. check(err == nil, "容量 10 页写入 %d 行无错误 (err=%v)", rows, err)
  166. case <-time.After(30 * time.Second):
  167. check(false, "写入超时未完成: 池子满时没有淘汰干净页 (疑似死锁)")
  168. return
  169. }
  170. n, err := tbl2.Count()
  171. check(err == nil && int(n) == rows, "淘汰后行数完整: 期望 %d, 实际 %d (err=%v)", rows, n, err)
  172. }
  173. // testReopenDurability 关掉再开: 上面的写入走的是 WAL 提交路径, 重开时要按 WAL 恢复
  174. // 回来, 一行都不能少。顺带覆盖"Close 会兜底把脏页落盘"这条。
  175. func testReopenDurability(tbl *DB.Table[Record], db *DB.DB) {
  176. fmt.Println("\n测试4: 关闭重开后数据仍在 (WAL 恢复)")
  177. before, err := tbl.Count()
  178. if err != nil {
  179. check(false, "关闭前查行数: %v", err)
  180. return
  181. }
  182. check(db.Close() == nil, "关闭数据库")
  183. reopened, err := DB.Open(dbPath)
  184. if err != nil {
  185. check(false, "重开 %s: %v", dbPath, err)
  186. return
  187. }
  188. defer func() {
  189. reopened.Close()
  190. os.Remove(dbPath)
  191. }()
  192. tbl2, err := DB.UseTable[Record](reopened, "test")
  193. if err != nil {
  194. check(false, "重开表: %v", err)
  195. return
  196. }
  197. after, err := tbl2.Count()
  198. check(err == nil && after == before, "重开后行数一致: %d → %d (err=%v)", before, after, err)
  199. }