| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- package pool
- import (
- "sync"
- "testing"
- )
- // newTestPool 构造一个带内存"磁盘"的缓冲池,便于观察缓存与落盘行为。
- func newTestPool(t *testing.T, capacity int) (*BufferPool, *diskStub) {
- t.Helper()
- d := &diskStub{pages: map[uint32][]byte{}}
- return NewBufferPool(capacity, d.read, d.write), d
- }
- type diskStub struct {
- mu sync.Mutex
- pages map[uint32][]byte
- }
- func (d *diskStub) read(pageID uint32, buf []byte) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- if data, ok := d.pages[pageID]; ok {
- copy(buf, data)
- }
- return nil
- }
- func (d *diskStub) write(pageID uint32, buf []byte) error {
- d.mu.Lock()
- defer d.mu.Unlock()
- data := make([]byte, len(buf))
- copy(data, buf)
- d.pages[pageID] = data
- return nil
- }
- // TestCollectDirtyKeepsDirtyPagesReadable 收集脏页快照之后, 脏页必须【仍然留在缓存里】
- // 且内容可读 —— 这是"并发写 + 提交"崩溃的直接回归点。
- //
- // 旧实现把脏页移出缓存, 于是从收集完到 WAL 逐页回写完成(中间隔着两次 fsync)这段窗口
- // 里, 这些页既不在缓存、也还没落盘: 并发的读一旦未命中就回源, 拿到的是旧内容, 对
- // 一页本会话刚分配、盘上从没写过的页则直接是全零。btree 把全零页当成 EntryLen==0 的
- // 节点, 接着就在 btPID 里切片越界 panic; 文件还没长到那个偏移时则读出错, 表现为
- // "insert: load failed"。
- //
- // 注意旧版本的这条用例恰好把 bug 当成预期钉住了: 它断言"脏页回落到磁盘, 期望 0"。
- func TestCollectDirtyKeepsDirtyPagesReadable(t *testing.T) {
- bp, _ := newTestPool(t, 64)
- _ = bp.WritePage(1, []byte{1}) // 脏页 (盘上还没有)
- bp.UpdatePage(2, []byte{9, 9}) // 干净页 (WAL 回填/回收页回填)
- _ = bp.WritePage(3, []byte{7}) // 脏页 (盘上还没有)
- if bp.Len() != 3 || bp.DirtyCount() != 2 {
- t.Fatalf("收集前期望池内 3 页 / 脏页 2, 实际 %d / %d", bp.Len(), bp.DirtyCount())
- }
- dirty := bp.CollectDirty()
- if len(dirty) != 2 {
- t.Fatalf("期望取出 2 个脏页, 实际 %d", len(dirty))
- }
- // 快照不等于"页离开了池子": 页数与脏页数都不该变。
- if bp.Len() != 3 {
- t.Fatalf("收集后页仍应全部留在池中, 实际 %d", bp.Len())
- }
- if bp.DirtyCount() != 2 {
- t.Fatalf("收集后脏页计数不该清零 (它们还没落盘), 实际 %d", bp.DirtyCount())
- }
- // 干净页 2 命中缓存
- out2 := make([]byte, 16)
- if err := bp.ReadPage(2, out2); err != nil {
- t.Fatalf("ReadPage(2): %v", err)
- }
- if out2[0] != 9 || out2[1] != 9 {
- t.Fatalf("干净页应命中缓存并返回回填值, 实际 %v", out2[:2])
- }
- // 脏页 1 / 3 也必须命中缓存并读到真实内容 —— 拿到 0 就说明又出现"两头都不在"。
- for _, c := range []struct {
- pageID uint32
- want byte
- }{{1, 1}, {3, 7}} {
- out := make([]byte, 16)
- if err := bp.ReadPage(c.pageID, out); err != nil {
- t.Fatalf("ReadPage(%d): %v", c.pageID, err)
- }
- if out[0] != c.want {
- t.Fatalf("收集快照后读取页 %d 应当是 %d, 实际 %d (页被移出缓存就会出现这种全零读)",
- c.pageID, c.want, out[0])
- }
- }
- }
- // TestMarkCleanOnlyClearsUnchangedPages 提交成功收脏标记时必须按版本号逐页判定:
- // 提交窗口里被写入线程重新改过的页要继续留脏。
- //
- // 反例就是 ClearAll 式的"提交成功 → 整个缓存标干净": 那些页并不在本笔 WAL 里, 标干净
- // 等于宣布"已持久化" —— 新内容再也不会被写盘, 页被淘汰时静默消失。
- func TestMarkCleanOnlyClearsUnchangedPages(t *testing.T) {
- bp, _ := newTestPool(t, 64)
- _ = bp.WritePage(1, []byte{1}) // 会被重新改脏
- _ = bp.WritePage(2, []byte{2}) // 保持原样
- dirty := bp.CollectDirty()
- if len(dirty) != 2 {
- t.Fatalf("期望 2 个脏页, 实际 %d", len(dirty))
- }
- // 模拟"提交进行中, 写入线程又改了页 1"
- _ = bp.WritePage(1, []byte{0xFF})
- // 提交成功 → WAL 回填步骤 (WritePageHome) 已在盘上写下快照内容
- bp.MarkClean(dirty)
- if bp.DirtyCount() != 1 {
- t.Fatalf("被重新改脏的页必须继续留脏, 期望脏页 1, 实际 %d", bp.DirtyCount())
- }
- out := make([]byte, 16)
- if err := bp.ReadPage(1, out); err != nil {
- t.Fatalf("ReadPage(1): %v", err)
- }
- if out[0] != 0xFF {
- t.Fatalf("页 1 应保留提交窗口里写入的新内容 0xFF, 实际 %d", out[0])
- }
- // 页 2 没被改过 → 转干净页 (内容不受影响)
- if entry, ok := bp.pages[2]; !ok || entry.dirty {
- t.Fatalf("未被改动的页应转成干净页, 实际 %+v (存在=%v)", entry, ok)
- }
- }
- // TestFlushWritesAndClearsDirty Flush 必须真的落盘, 并且把写出去的页转成干净页 ——
- // CollectDirty 不再移出缓存, 少了收尾这一步, 同一批内容会被下一次提交再写一遍。
- func TestFlushWritesAndClearsDirty(t *testing.T) {
- bp, disk := newTestPool(t, 64)
- _ = bp.WritePage(1, []byte{0x11})
- _ = bp.WritePage(2, []byte{0x22})
- bp.Flush()
- if bp.DirtyCount() != 0 {
- t.Fatalf("Flush 后不该还有脏页, 实际 %d", bp.DirtyCount())
- }
- for _, c := range []struct {
- pageID uint32
- want byte
- }{{1, 0x11}, {2, 0x22}} {
- got, ok := disk.pages[c.pageID]
- if !ok {
- t.Fatalf("页 %d 没有被写盘", c.pageID)
- }
- if got[0] != c.want {
- t.Fatalf("页 %d 落盘内容应当是 %#x, 实际 %#x", c.pageID, c.want, got[0])
- }
- }
- }
- // TestWritePageThresholdCountsOnlyDirty 验证自动提交阈值只由脏页数决定,
- // 常驻的干净页不会把阈值反复顶爆、导致提交频率失控。
- func TestWritePageThresholdCountsOnlyDirty(t *testing.T) {
- bp, _ := newTestPool(t, 1000)
- triggers := 0
- bp.SetDirtyThreshold(10, func() { triggers++ })
- // 灌入大量干净页 (模拟大量回收页经 UpdatePage 回填缓存)
- for i := uint32(0); i < 200; i++ {
- bp.UpdatePage(1000+i, []byte{1})
- }
- // 9 张脏页 —— 未达阈值,不应触发
- for i := uint32(0); i < 9; i++ {
- _ = bp.WritePage(i, []byte{2})
- }
- if triggers != 0 {
- t.Fatalf("干净页不应触发阈值, 实际触发 %d 次", triggers)
- }
- // 第 10 张脏页 → 命中阈值,触发一次
- _ = bp.WritePage(9, []byte{2})
- if triggers != 1 {
- t.Fatalf("脏页达阈值应触发 1 次, 实际 %d 次", triggers)
- }
- if bp.DirtyCount() != 10 {
- t.Fatalf("期望脏页计数 10, 实际 %d", bp.DirtyCount())
- }
- if bp.Len() < 200 {
- t.Fatalf("干净页应保留在池中, 实际池内仅 %d 页", bp.Len())
- }
- }
|