| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- package main
- import (
- "git.ycl.pub/yl32940/TsDB2/DB"
- "fmt"
- "os"
- "time"
- )
- type Record struct {
- DB.BaseModel
- Name string `vrtx:"maxlen:32"`
- }
- const (
- dbPath = "lock_table_test.db"
- poolPath = "lock_test2.db"
- )
- // failed 累计失败项。demo 用退出码说话: 只打印一句 "⚠️ 未检测到阻塞" 然后正常退出,
- // 等于把"没验到"当成"验过了" —— 这正是老版本测试2/3 的样子。
- var failed int
- func check(ok bool, format string, args ...any) {
- if ok {
- fmt.Printf(" ✅ "+format+"\n", args...)
- return
- }
- failed++
- fmt.Printf(" ❌ "+format+"\n", args...)
- }
- func main() {
- fmt.Println("==============================================")
- fmt.Println(" 锁表机制测试")
- fmt.Print("==============================================\n\n")
- os.Remove(dbPath)
- db, err := DB.Open(dbPath)
- if err != nil {
- panic(err)
- }
- tbl, err := DB.UseTable[Record](db, "test")
- if err != nil {
- panic(err)
- }
- testManualLock(tbl, db)
- testAutoCommitOnThreshold(tbl, db)
- testBufferPoolFull()
- testReopenDurability(tbl, db)
- }
- // testManualLock 钉住 README 注意事项 7 的语义: LockWrites 挡住的是【页落盘】,
- // 不是 Insert 本身 —— 锁定期插入被阻塞但不报错, 解锁后一条不少地进去。
- //
- // 老版本这里断言的是"锁定期插入应当被拒绝并返回错误"。那是错的: 引擎里根本没有
- // "写入被拒绝"这种错误码, BufferPool.WritePage 走的是阻塞等待 (waitForWriteLock)。
- // 于是插入在解锁后正常返回 err == nil, 断言"应为错误"必然成立不了 —— demo 一直是红的,
- // 而 README 又写着"全部通过"。
- func testManualLock(tbl *DB.Table[Record], db *DB.DB) {
- fmt.Println("测试1: 手动 LockWrites/UnlockWrites")
- before, err := tbl.Count()
- if err != nil {
- check(false, "前置查询行数: %v", err)
- return
- }
- db.LockWrites()
- fmt.Println(" ✅ 已锁定写入")
- type result struct {
- err error
- elapsed time.Duration
- }
- rec := &Record{Name: "Emp0000001"}
- done := make(chan result, 1)
- go func() {
- start := time.Now()
- insertErr := tbl.Insert(rec)
- done <- result{err: insertErr, elapsed: time.Since(start)}
- }()
- // 用"有没有结果"判断阻塞, 而不是用耗时阈值 —— 阈值只能证明"慢", 证明不了"被挡住"。
- var res result
- blocked := true
- select {
- case res = <-done:
- blocked = false
- case <-time.After(100 * time.Millisecond):
- }
- check(blocked, "锁定期插入被阻塞在页落盘处")
- db.UnlockWrites()
- fmt.Println(" ✅ 已解锁写入")
- if blocked {
- // 被挡住的那一支还没收到结果, 等它跑完再验 —— 否则下面查的是"插入之前"的行数。
- res = <-done
- }
- check(res.err == nil, "锁定期写入不报错 (err=%v, 解锁后 %v 完成)", res.err, res.elapsed)
- after, err := tbl.Count()
- check(err == nil && after == before+1, "解锁后真的落盘: %d → %d 行", before, after)
- if rec.ID != 0 {
- got, findErr := tbl.Find(rec.ID)
- check(findErr == nil && got != nil, "能按引擎分配到的 _id=%d 读回", rec.ID)
- } else {
- check(false, "插入后结构体没有回填 _id, 无法验证读回")
- }
- }
- // testAutoCommitOnThreshold 钉住真正的写入路径: 脏页阈值在 WritePage 里【同步】触发
- // Commit (pool.WritePage 在锁外直接调 onThreshold), 也就是"写到一半顺手提交一次"。
- // SetCommitPreset 的每一档都靠它, 200 行配 5 页阈值必然触发很多次。
- //
- // 为什么不用"一边写一边在另一个 goroutine 里 Commit": 引擎是【单写入者模型】
- // (见 pool/buffer_pool.go 里关于 capacityWait 死锁的注释), 写入与 Commit 必须由同一个
- // 写入者先后调用。跨 goroutine 重叠会踩到一个真问题: CollectDirty 把脏页移出缓存之后、
- // 回写磁盘之前, 这一页既不在缓存里也还不在盘上, 并发的读会从盘上读到全零页 ——
- // 实测稳定 panic (btPID: slice bounds out of range [-4:], 页头 EntryLen 读到 0)。
- // 那条路要等引擎补上写入串行化才能测。
- func testAutoCommitOnThreshold(tbl *DB.Table[Record], db *DB.DB) {
- fmt.Println("\n测试2: 写入过程中脏页阈值触发自动提交")
- db.SetAutoCommitDelay(0) // 关掉防抖计时器, 只留阈值触发, 保证提交是同步发生的
- db.SetDirtyThreshold(5) // 5 个脏页提交一次, 下面的行数必然触发多次
- before, err := tbl.Count()
- if err != nil {
- check(false, "前置查询行数: %v", err)
- return
- }
- const rows = 200
- for i := 0; i < rows; i++ {
- if insertErr := tbl.Insert(&Record{Name: fmt.Sprintf("Auto%06d", i)}); insertErr != nil {
- check(false, "写入第 %d 行时失败: %v", i+1, insertErr)
- return
- }
- }
- after, err := tbl.Count()
- check(err == nil && after == before+rows, "穿插 %d 次阈值提交后一行不丢: %d → %d",
- rows/5, before, after)
- }
- // testBufferPoolFull 钉住 README 注意事项 7 的第三条: 池子满时淘汰干净页而不是死锁。
- //
- // 老版本同样只看耗时 (> 50ms), 既证明不了"淘汰了干净页", 也证明不了"不死锁"—— 真死锁
- // 了它只会永远挂住。这里改成两条硬断言: 写入必须在时限内跑完 (超时即失败, 把"挂住"
- // 变成"报错"), 并且淘汰之后每一行都还得读得回来 (淘汰逻辑不能吃掉数据)。
- func testBufferPoolFull() {
- fmt.Println("\n测试3: BufferPool 满时淘汰干净页 (容量 10 页)")
- os.Remove(poolPath)
- db2, err := DB.Open(poolPath)
- if err != nil {
- check(false, "打开 %s: %v", poolPath, err)
- return
- }
- defer func() {
- db2.Close()
- os.Remove(poolPath)
- }()
- db2.SetBufferPoolCapacity(10) // 只允许 10 页
- tbl2, err := DB.UseTable[Record](db2, "test")
- if err != nil {
- check(false, "建表: %v", err)
- return
- }
- const rows = 200
- done := make(chan error, 1)
- go func() {
- for i := 1; i <= rows; i++ {
- if insertErr := tbl2.Insert(&Record{Name: fmt.Sprintf("Emp%07d", i)}); insertErr != nil {
- done <- insertErr
- return
- }
- }
- done <- nil
- }()
- // 容量 10 页远小于 200 行占的页数, 插入必然要边走边淘汰; 给它一个上限,
- // 拿不到结果就是"没能淘汰干净页把自己憋死了"。
- select {
- case err := <-done:
- check(err == nil, "容量 10 页写入 %d 行无错误 (err=%v)", rows, err)
- case <-time.After(30 * time.Second):
- check(false, "写入超时未完成: 池子满时没有淘汰干净页 (疑似死锁)")
- return
- }
- n, err := tbl2.Count()
- check(err == nil && int(n) == rows, "淘汰后行数完整: 期望 %d, 实际 %d (err=%v)", rows, n, err)
- }
- // testReopenDurability 关掉再开: 上面的写入走的是 WAL 提交路径, 重开时要按 WAL 恢复
- // 回来, 一行都不能少。顺带覆盖"Close 会兜底把脏页落盘"这条。
- func testReopenDurability(tbl *DB.Table[Record], db *DB.DB) {
- fmt.Println("\n测试4: 关闭重开后数据仍在 (WAL 恢复)")
- before, err := tbl.Count()
- if err != nil {
- check(false, "关闭前查行数: %v", err)
- return
- }
- check(db.Close() == nil, "关闭数据库")
- reopened, err := DB.Open(dbPath)
- if err != nil {
- check(false, "重开 %s: %v", dbPath, err)
- return
- }
- defer func() {
- reopened.Close()
- os.Remove(dbPath)
- }()
- tbl2, err := DB.UseTable[Record](reopened, "test")
- if err != nil {
- check(false, "重开表: %v", err)
- return
- }
- after, err := tbl2.Count()
- check(err == nil && after == before, "重开后行数一致: %d → %d (err=%v)", before, after, err)
- }
|