buffer_pool_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. package pool
  2. import (
  3. "sync"
  4. "testing"
  5. )
  6. // newTestPool 构造一个带内存"磁盘"的缓冲池,便于观察缓存与落盘行为。
  7. func newTestPool(t *testing.T, capacity int) (*BufferPool, *diskStub) {
  8. t.Helper()
  9. d := &diskStub{pages: map[uint32][]byte{}}
  10. return NewBufferPool(capacity, d.read, d.write), d
  11. }
  12. type diskStub struct {
  13. mu sync.Mutex
  14. pages map[uint32][]byte
  15. }
  16. func (d *diskStub) read(pageID uint32, buf []byte) error {
  17. d.mu.Lock()
  18. defer d.mu.Unlock()
  19. if data, ok := d.pages[pageID]; ok {
  20. copy(buf, data)
  21. }
  22. return nil
  23. }
  24. func (d *diskStub) write(pageID uint32, buf []byte) error {
  25. d.mu.Lock()
  26. defer d.mu.Unlock()
  27. data := make([]byte, len(buf))
  28. copy(data, buf)
  29. d.pages[pageID] = data
  30. return nil
  31. }
  32. // TestCollectDirtyKeepsDirtyPagesReadable 收集脏页快照之后, 脏页必须【仍然留在缓存里】
  33. // 且内容可读 —— 这是"并发写 + 提交"崩溃的直接回归点。
  34. //
  35. // 旧实现把脏页移出缓存, 于是从收集完到 WAL 逐页回写完成(中间隔着两次 fsync)这段窗口
  36. // 里, 这些页既不在缓存、也还没落盘: 并发的读一旦未命中就回源, 拿到的是旧内容, 对
  37. // 一页本会话刚分配、盘上从没写过的页则直接是全零。btree 把全零页当成 EntryLen==0 的
  38. // 节点, 接着就在 btPID 里切片越界 panic; 文件还没长到那个偏移时则读出错, 表现为
  39. // "insert: load failed"。
  40. //
  41. // 注意旧版本的这条用例恰好把 bug 当成预期钉住了: 它断言"脏页回落到磁盘, 期望 0"。
  42. func TestCollectDirtyKeepsDirtyPagesReadable(t *testing.T) {
  43. bp, _ := newTestPool(t, 64)
  44. _ = bp.WritePage(1, []byte{1}) // 脏页 (盘上还没有)
  45. bp.UpdatePage(2, []byte{9, 9}) // 干净页 (WAL 回填/回收页回填)
  46. _ = bp.WritePage(3, []byte{7}) // 脏页 (盘上还没有)
  47. if bp.Len() != 3 || bp.DirtyCount() != 2 {
  48. t.Fatalf("收集前期望池内 3 页 / 脏页 2, 实际 %d / %d", bp.Len(), bp.DirtyCount())
  49. }
  50. dirty := bp.CollectDirty()
  51. if len(dirty) != 2 {
  52. t.Fatalf("期望取出 2 个脏页, 实际 %d", len(dirty))
  53. }
  54. // 快照不等于"页离开了池子": 页数与脏页数都不该变。
  55. if bp.Len() != 3 {
  56. t.Fatalf("收集后页仍应全部留在池中, 实际 %d", bp.Len())
  57. }
  58. if bp.DirtyCount() != 2 {
  59. t.Fatalf("收集后脏页计数不该清零 (它们还没落盘), 实际 %d", bp.DirtyCount())
  60. }
  61. // 干净页 2 命中缓存
  62. out2 := make([]byte, 16)
  63. if err := bp.ReadPage(2, out2); err != nil {
  64. t.Fatalf("ReadPage(2): %v", err)
  65. }
  66. if out2[0] != 9 || out2[1] != 9 {
  67. t.Fatalf("干净页应命中缓存并返回回填值, 实际 %v", out2[:2])
  68. }
  69. // 脏页 1 / 3 也必须命中缓存并读到真实内容 —— 拿到 0 就说明又出现"两头都不在"。
  70. for _, c := range []struct {
  71. pageID uint32
  72. want byte
  73. }{{1, 1}, {3, 7}} {
  74. out := make([]byte, 16)
  75. if err := bp.ReadPage(c.pageID, out); err != nil {
  76. t.Fatalf("ReadPage(%d): %v", c.pageID, err)
  77. }
  78. if out[0] != c.want {
  79. t.Fatalf("收集快照后读取页 %d 应当是 %d, 实际 %d (页被移出缓存就会出现这种全零读)",
  80. c.pageID, c.want, out[0])
  81. }
  82. }
  83. }
  84. // TestMarkCleanOnlyClearsUnchangedPages 提交成功收脏标记时必须按版本号逐页判定:
  85. // 提交窗口里被写入线程重新改过的页要继续留脏。
  86. //
  87. // 反例就是 ClearAll 式的"提交成功 → 整个缓存标干净": 那些页并不在本笔 WAL 里, 标干净
  88. // 等于宣布"已持久化" —— 新内容再也不会被写盘, 页被淘汰时静默消失。
  89. func TestMarkCleanOnlyClearsUnchangedPages(t *testing.T) {
  90. bp, _ := newTestPool(t, 64)
  91. _ = bp.WritePage(1, []byte{1}) // 会被重新改脏
  92. _ = bp.WritePage(2, []byte{2}) // 保持原样
  93. dirty := bp.CollectDirty()
  94. if len(dirty) != 2 {
  95. t.Fatalf("期望 2 个脏页, 实际 %d", len(dirty))
  96. }
  97. // 模拟"提交进行中, 写入线程又改了页 1"
  98. _ = bp.WritePage(1, []byte{0xFF})
  99. // 提交成功 → WAL 回填步骤 (WritePageHome) 已在盘上写下快照内容
  100. bp.MarkClean(dirty)
  101. if bp.DirtyCount() != 1 {
  102. t.Fatalf("被重新改脏的页必须继续留脏, 期望脏页 1, 实际 %d", bp.DirtyCount())
  103. }
  104. out := make([]byte, 16)
  105. if err := bp.ReadPage(1, out); err != nil {
  106. t.Fatalf("ReadPage(1): %v", err)
  107. }
  108. if out[0] != 0xFF {
  109. t.Fatalf("页 1 应保留提交窗口里写入的新内容 0xFF, 实际 %d", out[0])
  110. }
  111. // 页 2 没被改过 → 转干净页 (内容不受影响)
  112. if entry, ok := bp.pages[2]; !ok || entry.dirty {
  113. t.Fatalf("未被改动的页应转成干净页, 实际 %+v (存在=%v)", entry, ok)
  114. }
  115. }
  116. // TestFlushWritesAndClearsDirty Flush 必须真的落盘, 并且把写出去的页转成干净页 ——
  117. // CollectDirty 不再移出缓存, 少了收尾这一步, 同一批内容会被下一次提交再写一遍。
  118. func TestFlushWritesAndClearsDirty(t *testing.T) {
  119. bp, disk := newTestPool(t, 64)
  120. _ = bp.WritePage(1, []byte{0x11})
  121. _ = bp.WritePage(2, []byte{0x22})
  122. bp.Flush()
  123. if bp.DirtyCount() != 0 {
  124. t.Fatalf("Flush 后不该还有脏页, 实际 %d", bp.DirtyCount())
  125. }
  126. for _, c := range []struct {
  127. pageID uint32
  128. want byte
  129. }{{1, 0x11}, {2, 0x22}} {
  130. got, ok := disk.pages[c.pageID]
  131. if !ok {
  132. t.Fatalf("页 %d 没有被写盘", c.pageID)
  133. }
  134. if got[0] != c.want {
  135. t.Fatalf("页 %d 落盘内容应当是 %#x, 实际 %#x", c.pageID, c.want, got[0])
  136. }
  137. }
  138. }
  139. // TestWritePageThresholdCountsOnlyDirty 验证自动提交阈值只由脏页数决定,
  140. // 常驻的干净页不会把阈值反复顶爆、导致提交频率失控。
  141. func TestWritePageThresholdCountsOnlyDirty(t *testing.T) {
  142. bp, _ := newTestPool(t, 1000)
  143. triggers := 0
  144. bp.SetDirtyThreshold(10, func() { triggers++ })
  145. // 灌入大量干净页 (模拟大量回收页经 UpdatePage 回填缓存)
  146. for i := uint32(0); i < 200; i++ {
  147. bp.UpdatePage(1000+i, []byte{1})
  148. }
  149. // 9 张脏页 —— 未达阈值,不应触发
  150. for i := uint32(0); i < 9; i++ {
  151. _ = bp.WritePage(i, []byte{2})
  152. }
  153. if triggers != 0 {
  154. t.Fatalf("干净页不应触发阈值, 实际触发 %d 次", triggers)
  155. }
  156. // 第 10 张脏页 → 命中阈值,触发一次
  157. _ = bp.WritePage(9, []byte{2})
  158. if triggers != 1 {
  159. t.Fatalf("脏页达阈值应触发 1 次, 实际 %d 次", triggers)
  160. }
  161. if bp.DirtyCount() != 10 {
  162. t.Fatalf("期望脏页计数 10, 实际 %d", bp.DirtyCount())
  163. }
  164. if bp.Len() < 200 {
  165. t.Fatalf("干净页应保留在池中, 实际池内仅 %d 页", bp.Len())
  166. }
  167. }
  168. // countingRead 包装 diskStub.read 计数 (用于断言"命中缓存后不再走磁盘")。
  169. type countingRead struct {
  170. d *diskStub
  171. n int
  172. }
  173. func (c *countingRead) read(pageID uint32, buf []byte) error {
  174. c.n++
  175. return c.d.read(pageID, buf)
  176. }
  177. // TestReadPageCachesCleanPages 读未命中必须把磁盘内容作为干净页载入缓存:
  178. // 第二次读同一页必须命中缓存 (磁盘读次数不增加), 这是"LRU BufferPool"读路径
  179. // 收益的主体 (B+ 树下降 / Hash 目录链 / 堆页 Load 都是重复读)。
  180. func TestReadPageCachesCleanPages(t *testing.T) {
  181. d := &diskStub{pages: map[uint32][]byte{42: {0x2A}}}
  182. cr := &countingRead{d: d}
  183. bp := NewBufferPool(8, cr.read, d.write)
  184. buf := make([]byte, 1)
  185. if err := bp.ReadPage(42, buf); err != nil {
  186. t.Fatalf("第一次读: %v", err)
  187. }
  188. if buf[0] != 0x2A {
  189. t.Fatalf("读到 %d, 想要 0x2A", buf[0])
  190. }
  191. if cr.n != 1 {
  192. t.Fatalf("第一次读应走 1 次磁盘, 实际 %d", cr.n)
  193. }
  194. // 第二次读: 必须命中缓存
  195. if err := bp.ReadPage(42, buf); err != nil {
  196. t.Fatalf("第二次读: %v", err)
  197. }
  198. if cr.n != 1 {
  199. t.Fatalf("第二次读应命中缓存 (磁盘读仍为 1), 实际 %d", cr.n)
  200. }
  201. // 载入的是干净页: 不占脏页计数, 可被 LRU 淘汰
  202. if bp.DirtyCount() != 0 {
  203. t.Fatalf("读缓存页不应计脏, 实际脏页 %d", bp.DirtyCount())
  204. }
  205. }
  206. // TestLRUEvictsColdestPage LRU 淘汰必须按"最久未用"顺序: 最近读过的热页留下,
  207. // 最冷的页被淘汰。旧实现按 map 随机序淘汰, 热页与冷页同等概率被丢。
  208. func TestLRUEvictsColdestPage(t *testing.T) {
  209. d := &diskStub{pages: map[uint32][]byte{}}
  210. cr := &countingRead{d: d}
  211. bp := NewBufferPool(3, cr.read, d.write)
  212. // 三张干净页入池 (模拟读缓存), lru 序: [1, 2, 3]
  213. bp.UpdatePage(1, []byte{1})
  214. bp.UpdatePage(2, []byte{2})
  215. bp.UpdatePage(3, []byte{3})
  216. // 读页 1 → 它变热: lru 序 [2, 3, 1]
  217. if err := bp.ReadPage(1, make([]byte, 1)); err != nil {
  218. t.Fatalf("ReadPage(1): %v", err)
  219. }
  220. // 写页 4 (触发容量淘汰): 最冷的页 2 必须先被淘汰
  221. if err := bp.WritePage(4, []byte{4}); err != nil {
  222. t.Fatalf("WritePage(4): %v", err)
  223. }
  224. if _, ok := bp.pages[2]; ok {
  225. t.Fatalf("最久未用的页 2 应被淘汰, 却仍在池中")
  226. }
  227. for _, pid := range []uint32{1, 3, 4} {
  228. if _, ok := bp.pages[pid]; !ok {
  229. t.Fatalf("页 %d 应在池中 (淘汰顺序错误)", pid)
  230. }
  231. }
  232. // 淘汰后链表与 map 必须一致 (无孤儿节点)
  233. if bp.lru.Len() != len(bp.pages) {
  234. t.Fatalf("lru 链表 (%d) 与页表 (%d) 长度不一致", bp.lru.Len(), len(bp.pages))
  235. }
  236. // 当前 lru 序 [3, 1, 4]: 读页 2 (miss, 重新入池) 应淘汰最冷的页 3
  237. cr.n = 0
  238. if err := bp.ReadPage(2, make([]byte, 1)); err != nil {
  239. t.Fatalf("ReadPage(2): %v", err)
  240. }
  241. if cr.n != 1 {
  242. t.Fatalf("被淘汰的页 2 应重新走磁盘 1 次, 实际 %d 次", cr.n)
  243. }
  244. if _, ok := bp.pages[3]; ok {
  245. t.Fatalf("页 3 现在是最冷的, 应被页 2 的入池挤掉")
  246. }
  247. if err := bp.ReadPage(1, make([]byte, 1)); err != nil {
  248. t.Fatalf("ReadPage(1): %v", err)
  249. }
  250. if cr.n != 1 {
  251. t.Fatalf("热页 1 应命中缓存, 实际磁盘读 %d 次", cr.n)
  252. }
  253. }
  254. // TestReadPageNeverOverwritesConcurrentDirty 读盘期间另一线程写入了同一页时,
  255. // ReadPage 必须以池内 (更新) 内容为准 —— 绝不能用更旧的读盘结果覆盖脏页。
  256. func TestReadPageNeverOverwritesConcurrentDirty(t *testing.T) {
  257. d := &diskStub{pages: map[uint32][]byte{7: {0x07}}}
  258. started := make(chan struct{}, 1)
  259. release := make(chan struct{})
  260. bp := NewBufferPool(8, func(pageID uint32, buf []byte) error {
  261. started <- struct{}{}
  262. <-release // 卡住读盘, 制造并发窗口
  263. return d.read(pageID, buf)
  264. }, d.write)
  265. out := make([]byte, 1)
  266. done := make(chan error, 1)
  267. go func() { done <- bp.ReadPage(7, out) }()
  268. <-started // 读盘进行中 (页 7 尚未入池)
  269. // 并发写入同页 (单写入者模型下不该发生, 但缓冲池是并发的最后防线)
  270. if err := bp.WritePage(7, []byte{0x99}); err != nil {
  271. t.Fatalf("WritePage(7): %v", err)
  272. }
  273. close(release)
  274. if err := <-done; err != nil {
  275. t.Fatalf("ReadPage(7): %v", err)
  276. }
  277. if out[0] != 0x99 {
  278. t.Fatalf("读到的是旧读盘内容 %#x, 池内新内容 0x99 被覆盖了", out[0])
  279. }
  280. if bp.DirtyCount() != 1 {
  281. t.Fatalf("页 7 必须保持脏, 实际脏页 %d", bp.DirtyCount())
  282. }
  283. // 再读一次: 必须命中且仍是新内容
  284. if err := bp.ReadPage(7, out); err != nil {
  285. t.Fatalf("重读: %v", err)
  286. }
  287. if out[0] != 0x99 {
  288. t.Fatalf("重读拿到 %#x", out[0])
  289. }
  290. }
  291. // TestWriteOverReadCachedPageKeepsLRUConsistent 读缓存的干净页被 WritePage 改脏后,
  292. // 条目原地复用: lru 链表与页表保持一致, 不泄漏孤儿节点。
  293. func TestWriteOverReadCachedPageKeepsLRUConsistent(t *testing.T) {
  294. d := &diskStub{pages: map[uint32][]byte{5: {0x05}}}
  295. bp := NewBufferPool(8, d.read, d.write)
  296. // 读入池 (干净页)
  297. if err := bp.ReadPage(5, make([]byte, 1)); err != nil {
  298. t.Fatalf("ReadPage(5): %v", err)
  299. }
  300. // 改脏
  301. if err := bp.WritePage(5, []byte{0x55}); err != nil {
  302. t.Fatalf("WritePage(5): %v", err)
  303. }
  304. if bp.DirtyCount() != 1 {
  305. t.Fatalf("改脏后脏页应计 1, 实际 %d", bp.DirtyCount())
  306. }
  307. if bp.lru.Len() != len(bp.pages) {
  308. t.Fatalf("lru 链表 (%d) 与页表 (%d) 长度不一致", bp.lru.Len(), len(bp.pages))
  309. }
  310. // 淘汰路径上它现在是脏页: 容量满时不得被淘汰
  311. bp.UpdatePage(100, []byte{1})
  312. if err := bp.WritePage(6, []byte{6}); err != nil {
  313. t.Fatalf("WritePage(6): %v", err)
  314. }
  315. if _, ok := bp.pages[5]; !ok {
  316. t.Fatalf("脏页 5 绝不能被容量淘汰")
  317. }
  318. }