A B+Tree Storage Engine
1package buffer
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "path"
8 "testing"
9
10 "github.com/jobala/petro/storage/disk"
11 "github.com/stretchr/testify/assert"
12)
13
14func TestBufferPoolManager(t *testing.T) {
15 t.Run("reads a page from disk", func(t *testing.T) {
16 file := CreateDbFile(t)
17 t.Cleanup(func() {
18 _ = os.Remove(file.Name())
19 })
20
21 replacer := NewLrukReplacer(5, 2)
22 diskMgr := disk.NewManager(file)
23 diskScheduler := disk.NewScheduler(diskMgr)
24 bufferMgr := NewBufferpoolManager(5, replacer, diskScheduler)
25
26 pageId := 1
27 data := make([]byte, disk.PAGE_SIZE)
28 copy(data, []byte("hello, world!"))
29 syncWrite(pageId, data, diskScheduler)
30
31 pageGuard, err := bufferMgr.ReadPage(int64(pageId))
32 defer pageGuard.Drop()
33 assert.NoError(t, err)
34
35 assert.Equal(t, data, pageGuard.GetData())
36 assert.Equal(t, data, bufferMgr.frames[0].data)
37 })
38
39 t.Run("evicts least recently used page", func(t *testing.T) {
40 file := CreateDbFile(t)
41 t.Cleanup(func() {
42 _ = os.Remove(file.Name())
43 })
44
45 replacer := NewLrukReplacer(2, 2)
46 diskMgr := disk.NewManager(file)
47 diskScheduler := disk.NewScheduler(diskMgr)
48 bufferMgr := NewBufferpoolManager(2, replacer, diskScheduler)
49
50 content := []string{"1", "2", "3"}
51 for pageId, d := range content {
52 data := make([]byte, disk.PAGE_SIZE)
53 copy(data, []byte(d))
54 syncWrite(pageId+1, data, diskScheduler)
55 }
56
57 // access page 2 many times
58 for range 5 {
59 pageGuard, err := bufferMgr.ReadPage(int64(2))
60 assert.NoError(t, err)
61 pageGuard.Drop()
62 }
63
64 // access page 1 to make page 2 least recently used
65 pageGuard, err := bufferMgr.ReadPage(int64(1))
66 assert.NoError(t, err)
67 pageGuard.Drop()
68
69 // accessing page 3 should evict page 1
70 for i := range len(content) {
71 pageGuard, err := bufferMgr.ReadPage(int64(i + 1))
72
73 assert.NoError(t, err)
74 assert.Equal(t, string(bytes.Trim(pageGuard.GetData(), "\x00")), content[i])
75 pageGuard.Drop()
76 }
77
78 // page id 1, should have been evicted
79 assert.Equal(t, bufferMgr.frames[0].pageId, int64(2))
80 assert.Equal(t, bufferMgr.frames[1].pageId, int64(3))
81
82 // buffermanager's pagetable shouldn't have evicted pageId
83 _, ok := bufferMgr.pageTable[1]
84 assert.Equal(t, false, ok)
85 })
86
87 t.Run("writes a page to disk", func(t *testing.T) {
88 file := CreateDbFile(t)
89 t.Cleanup(func() {
90 _ = os.Remove(file.Name())
91 })
92
93 replacer := NewLrukReplacer(5, 2)
94 diskMgr := disk.NewManager(file)
95 diskScheduler := disk.NewScheduler(diskMgr)
96 bufferMgr := NewBufferpoolManager(5, replacer, diskScheduler)
97
98 pageId := 1
99 data := make([]byte, disk.PAGE_SIZE)
100 copy(data, []byte("hello, world!"))
101
102 pageGuard, err := bufferMgr.WritePage(int64(pageId))
103 copy(*pageGuard.GetDataMut(), data)
104 defer pageGuard.Drop()
105
106 assert.NoError(t, err)
107 assert.Equal(t, data, bufferMgr.frames[0].data)
108 assert.True(t, bufferMgr.frames[0].dirty, true)
109
110 bufferMgr.flush(bufferMgr.frames[0])
111 res := syncRead(pageId, diskScheduler)
112 assert.Equal(t, data, res)
113 })
114
115 t.Run("dirty evicted pages are flushed to disk", func(t *testing.T) {
116 file := CreateDbFile(t)
117 t.Cleanup(func() {
118 _ = os.Remove(file.Name())
119 })
120
121 replacer := NewLrukReplacer(2, 2)
122 diskMgr := disk.NewManager(file)
123 diskScheduler := disk.NewScheduler(diskMgr)
124 bufferMgr := NewBufferpoolManager(2, replacer, diskScheduler)
125
126 content := []string{"1", "2", "3"}
127 for pageId, d := range content {
128 data := make([]byte, disk.PAGE_SIZE)
129 copy(data, []byte(d))
130
131 pageGuard, err := bufferMgr.WritePage(int64(pageId + 1))
132 copy(*pageGuard.GetDataMut(), data)
133 pageGuard.Drop()
134
135 assert.NoError(t, err)
136 }
137
138 // page 1 should have been evicted and flushed to disk
139 res := syncRead(1, diskScheduler)
140 assert.Equal(t, content[0], string(bytes.Trim(res, "\x00")))
141 })
142
143 t.Run("can read and write", func(t *testing.T) {
144 file := CreateDbFile(t)
145 t.Cleanup(func() {
146 _ = os.Remove(file.Name())
147 })
148
149 replacer := NewLrukReplacer(2, 2)
150 diskMgr := disk.NewManager(file)
151 diskScheduler := disk.NewScheduler(diskMgr)
152 bufferMgr := NewBufferpoolManager(2, replacer, diskScheduler)
153
154 content := []string{"1", "2", "3"}
155 for pageId, d := range content {
156 data := make([]byte, disk.PAGE_SIZE)
157 copy(data, []byte(d))
158 pageGuard, err := bufferMgr.WritePage(int64(pageId + 1))
159 copy(*pageGuard.GetDataMut(), data)
160 pageGuard.Drop()
161
162 assert.NoError(t, err)
163 }
164
165 for pageId, data := range content {
166 pageGuard, err := bufferMgr.ReadPage(int64(pageId + 1))
167 pageGuard.Drop()
168
169 assert.NoError(t, err)
170 assert.Equal(t, data, string(bytes.Trim(pageGuard.GetData(), "\x00")))
171 }
172 })
173}
174
175func CreateDbFile(t *testing.T) *os.File {
176 t.Helper()
177 dbFile := path.Join(t.TempDir(), "test.db")
178
179 file, err := os.OpenFile(dbFile, os.O_CREATE|os.O_RDWR, 0644)
180 if err != nil {
181 panic(fmt.Sprintf("failed creating db file\n%v", err))
182 }
183
184 // create 4kb file
185 _ = os.Truncate(file.Name(), disk.PAGE_SIZE)
186 fileInfo, err := os.Stat(file.Name())
187 assert.NoError(t, err)
188 assert.Equal(t, int64(disk.PAGE_SIZE), fileInfo.Size())
189 return file
190}
191
192func syncWrite(pageId int, data []byte, diskScheduler *disk.DiskScheduler) {
193 resCh := make(chan disk.DiskResp)
194
195 writeReq := disk.DiskReq{
196 PageId: pageId,
197 Write: true,
198 Data: data,
199 RespCh: resCh,
200 }
201
202 diskScheduler.Schedule(writeReq)
203 <-resCh
204}
205
206func syncRead(pageId int, diskScheduler *disk.DiskScheduler) []byte {
207 readReq := disk.NewRequest(int64(pageId), nil, false)
208 respCh := diskScheduler.Schedule(readReq)
209 res := <-respCh
210
211 return res.Data
212}