Skip to main content

mentedb_storage/
buffer.rs

1//! Buffer Pool: in-memory page cache with CLOCK eviction.
2//!
3//! Pages are loaded from disk into fixed-capacity frame slots. Pin counts
4//! prevent eviction of pages currently in use. The CLOCK algorithm sweeps
5//! frames looking for an unpinned, unreferenced victim when the pool is full.
6
7use ahash::AHashMap;
8use parking_lot::Mutex;
9
10use crate::page::{Page, PageId, PageManager};
11use mentedb_core::error::{MenteError, MenteResult};
12use tracing::{debug, trace};
13
14type FrameId = usize;
15
16/// A single frame in the buffer pool.
17struct Frame {
18    page: Box<Page>,
19    page_id: Option<PageId>,
20    pin_count: u32,
21    dirty: bool,
22    /// CLOCK reference bit: set on access, cleared by the sweep hand.
23    reference: bool,
24}
25
26impl Frame {
27    fn new() -> Self {
28        Self {
29            page: Box::new(Page::zeroed()),
30            page_id: None,
31            pin_count: 0,
32            dirty: false,
33            reference: false,
34        }
35    }
36}
37
38struct BufferPoolInner {
39    frames: Vec<Frame>,
40    page_table: AHashMap<PageId, FrameId>,
41    clock_hand: usize,
42    capacity: usize,
43}
44
45/// Thread-safe buffer pool with CLOCK eviction.
46pub struct BufferPool {
47    inner: Mutex<BufferPoolInner>,
48}
49
50impl BufferPool {
51    /// Create a buffer pool with `capacity` frame slots.
52    pub fn new(capacity: usize) -> Self {
53        assert!(capacity > 0, "buffer pool capacity must be > 0");
54        let frames = (0..capacity).map(|_| Frame::new()).collect();
55        Self {
56            inner: Mutex::new(BufferPoolInner {
57                frames,
58                page_table: AHashMap::with_capacity(capacity),
59                clock_hand: 0,
60                capacity,
61            }),
62        }
63    }
64
65    /// Fetch a page into the pool (loading from disk if necessary).
66    ///
67    /// The page is automatically pinned (pin_count incremented).
68    /// Caller must call `unpin_page` when done.
69    pub fn fetch_page(&self, page_id: PageId, pm: &mut PageManager) -> MenteResult<Box<Page>> {
70        let mut inner = self.inner.lock();
71
72        // Cache hit
73        if let Some(&frame_id) = inner.page_table.get(&page_id) {
74            let frame = &mut inner.frames[frame_id];
75            frame.pin_count += 1;
76            frame.reference = true;
77            trace!(page_id = page_id.0, frame_id, "buffer pool hit");
78            return Ok(frame.page.clone());
79        }
80
81        // Cache miss — find a victim frame.
82        let frame_id = Self::find_victim(&mut inner)?;
83
84        // Flush dirty victim if needed.
85        if inner.frames[frame_id].dirty
86            && let Some(old_pid) = inner.frames[frame_id].page_id
87        {
88            pm.write_page(old_pid, &inner.frames[frame_id].page)?;
89            debug!(page_id = old_pid.0, frame_id, "flushed dirty victim");
90        }
91
92        // Remove old mapping.
93        if let Some(old_pid) = inner.frames[frame_id].page_id {
94            inner.page_table.remove(&old_pid);
95        }
96
97        // Load the requested page from disk.
98        let page = pm.read_page(page_id)?;
99        {
100            let frame = &mut inner.frames[frame_id];
101            *frame.page = *page;
102            frame.page_id = Some(page_id);
103            frame.pin_count = 1;
104            frame.dirty = false;
105            frame.reference = true;
106        }
107
108        inner.page_table.insert(page_id, frame_id);
109        trace!(
110            page_id = page_id.0,
111            frame_id, "loaded page into buffer pool"
112        );
113
114        Ok(inner.frames[frame_id].page.clone())
115    }
116
117    /// Increment the pin count of a page already in the pool.
118    pub fn pin_page(&self, page_id: PageId) -> MenteResult<()> {
119        let mut inner = self.inner.lock();
120        match inner.page_table.get(&page_id) {
121            Some(&fid) => {
122                inner.frames[fid].pin_count += 1;
123                Ok(())
124            }
125            None => Err(MenteError::Storage(format!(
126                "page {} not in buffer pool",
127                page_id.0
128            ))),
129        }
130    }
131
132    /// Decrement pin count and optionally mark the page dirty.
133    pub fn unpin_page(&self, page_id: PageId, dirty: bool) -> MenteResult<()> {
134        let mut inner = self.inner.lock();
135        match inner.page_table.get(&page_id) {
136            Some(&fid) => {
137                let frame = &mut inner.frames[fid];
138                if frame.pin_count > 0 {
139                    frame.pin_count -= 1;
140                }
141                if dirty {
142                    frame.dirty = true;
143                }
144                Ok(())
145            }
146            None => Err(MenteError::Storage(format!(
147                "page {} not in buffer pool",
148                page_id.0
149            ))),
150        }
151    }
152
153    /// Drop a page from the pool without flushing, discarding any dirty state.
154    ///
155    /// Used when a page is freed: the on-disk state is already authoritative,
156    /// so a stale cached copy must never be served or flushed back. A no-op if
157    /// the page is not cached.
158    pub fn invalidate(&self, page_id: PageId) {
159        let mut inner = self.inner.lock();
160        if let Some(fid) = inner.page_table.remove(&page_id) {
161            let frame = &mut inner.frames[fid];
162            frame.page_id = None;
163            frame.pin_count = 0;
164            frame.dirty = false;
165            frame.reference = false;
166            debug!(page_id = page_id.0, "invalidated cached page");
167        }
168    }
169
170    /// Replace the cached copy of a page and mark it dirty.
171    pub fn update_page(&self, page_id: PageId, page: &Page) -> MenteResult<()> {
172        let mut inner = self.inner.lock();
173        match inner.page_table.get(&page_id) {
174            Some(&fid) => {
175                let frame = &mut inner.frames[fid];
176                *frame.page = page.clone();
177                frame.dirty = true;
178                Ok(())
179            }
180            None => Err(MenteError::Storage(format!(
181                "page {} not in buffer pool",
182                page_id.0
183            ))),
184        }
185    }
186
187    /// Flush a single dirty page to disk.
188    pub fn flush_page(&self, page_id: PageId, pm: &mut PageManager) -> MenteResult<()> {
189        let mut inner = self.inner.lock();
190        match inner.page_table.get(&page_id) {
191            Some(&fid) => {
192                let frame = &mut inner.frames[fid];
193                if frame.dirty {
194                    pm.write_page(page_id, &frame.page)?;
195                    frame.dirty = false;
196                    debug!(page_id = page_id.0, "flushed page");
197                }
198                Ok(())
199            }
200            None => Err(MenteError::Storage(format!(
201                "page {} not in buffer pool",
202                page_id.0
203            ))),
204        }
205    }
206
207    /// Flush all dirty pages to disk.
208    pub fn flush_all(&self, pm: &mut PageManager) -> MenteResult<()> {
209        let mut inner = self.inner.lock();
210        for frame in &mut inner.frames {
211            if frame.dirty
212                && let Some(pid) = frame.page_id
213            {
214                pm.write_page(pid, &frame.page)?;
215                frame.dirty = false;
216            }
217        }
218        debug!("flushed all dirty pages");
219        Ok(())
220    }
221
222    /// CLOCK eviction: find an unpinned, unreferenced frame.
223    fn find_victim(inner: &mut BufferPoolInner) -> MenteResult<FrameId> {
224        let cap = inner.capacity;
225
226        // Prefer an empty frame first.
227        for i in 0..cap {
228            if inner.frames[i].page_id.is_none() {
229                return Ok(i);
230            }
231        }
232
233        // CLOCK sweep — at most 2 full rotations.
234        let max_sweeps = cap * 2;
235        for _ in 0..max_sweeps {
236            let idx = inner.clock_hand;
237            inner.clock_hand = (inner.clock_hand + 1) % cap;
238
239            let frame = &mut inner.frames[idx];
240            if frame.pin_count == 0 {
241                if !frame.reference {
242                    return Ok(idx);
243                }
244                frame.reference = false;
245            }
246        }
247
248        Err(MenteError::Storage(
249            "buffer pool full: all pages are pinned".into(),
250        ))
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::page::Page;
258
259    fn setup() -> (tempfile::TempDir, PageManager) {
260        let dir = tempfile::tempdir().unwrap();
261        let pm = PageManager::open(dir.path()).unwrap();
262        (dir, pm)
263    }
264
265    #[test]
266    fn test_fetch_and_cache_hit() {
267        let (_dir, mut pm) = setup();
268        let pool = BufferPool::new(4);
269
270        let pid = pm.allocate_page().unwrap();
271        let mut page = Page::zeroed();
272        page.header.page_id = pid.0;
273        page.data[0..3].copy_from_slice(b"abc");
274        pm.write_page(pid, &page).unwrap();
275
276        // First fetch — cache miss, loads from disk.
277        let p1 = pool.fetch_page(pid, &mut pm).unwrap();
278        assert_eq!(&p1.data[0..3], b"abc");
279
280        // Unpin.
281        pool.unpin_page(pid, false).unwrap();
282
283        // Second fetch — cache hit.
284        let p2 = pool.fetch_page(pid, &mut pm).unwrap();
285        assert_eq!(&p2.data[0..3], b"abc");
286        pool.unpin_page(pid, false).unwrap();
287    }
288
289    #[test]
290    fn test_dirty_flush() {
291        let (_dir, mut pm) = setup();
292        let pool = BufferPool::new(4);
293
294        let pid = pm.allocate_page().unwrap();
295
296        let mut page = Page::zeroed();
297        page.header.page_id = pid.0;
298        page.data[0] = 42;
299        pm.write_page(pid, &page).unwrap();
300
301        // Fetch, modify, mark dirty.
302        let _ = pool.fetch_page(pid, &mut pm).unwrap();
303        let mut modified = Page::zeroed();
304        modified.header.page_id = pid.0;
305        modified.data[0] = 99;
306        pool.update_page(pid, &modified).unwrap();
307        pool.unpin_page(pid, true).unwrap();
308
309        // Flush to disk.
310        pool.flush_page(pid, &mut pm).unwrap();
311
312        // Read directly from disk to verify.
313        let on_disk = pm.read_page(pid).unwrap();
314        assert_eq!(on_disk.data[0], 99);
315    }
316
317    #[test]
318    fn test_eviction() {
319        let (_dir, mut pm) = setup();
320        let pool = BufferPool::new(2); // tiny pool
321
322        // Allocate 3 pages.
323        let p1 = pm.allocate_page().unwrap();
324        let p2 = pm.allocate_page().unwrap();
325        let p3 = pm.allocate_page().unwrap();
326
327        for pid in [p1, p2, p3] {
328            let mut page = Page::zeroed();
329            page.header.page_id = pid.0;
330            page.data[0] = pid.0 as u8;
331            pm.write_page(pid, &page).unwrap();
332        }
333
334        // Fill pool with p1, p2.
335        let _ = pool.fetch_page(p1, &mut pm).unwrap();
336        pool.unpin_page(p1, false).unwrap();
337        let _ = pool.fetch_page(p2, &mut pm).unwrap();
338        pool.unpin_page(p2, false).unwrap();
339
340        // Fetching p3 must evict one of the above.
341        let page3 = pool.fetch_page(p3, &mut pm).unwrap();
342        assert_eq!(page3.data[0], p3.0 as u8);
343        pool.unpin_page(p3, false).unwrap();
344    }
345
346    #[test]
347    fn test_all_pinned_error() {
348        let (_dir, mut pm) = setup();
349        let pool = BufferPool::new(2);
350
351        let p1 = pm.allocate_page().unwrap();
352        let p2 = pm.allocate_page().unwrap();
353        let p3 = pm.allocate_page().unwrap();
354
355        for pid in [p1, p2, p3] {
356            let mut page = Page::zeroed();
357            page.header.page_id = pid.0;
358            pm.write_page(pid, &page).unwrap();
359        }
360
361        // Pin both frames (don't unpin).
362        let _ = pool.fetch_page(p1, &mut pm).unwrap();
363        let _ = pool.fetch_page(p2, &mut pm).unwrap();
364
365        // p3 should fail — no victim available.
366        assert!(pool.fetch_page(p3, &mut pm).is_err());
367    }
368
369    #[test]
370    fn test_flush_all() {
371        let (_dir, mut pm) = setup();
372        let pool = BufferPool::new(4);
373
374        let p1 = pm.allocate_page().unwrap();
375        let p2 = pm.allocate_page().unwrap();
376
377        for pid in [p1, p2] {
378            let mut page = Page::zeroed();
379            page.header.page_id = pid.0;
380            pm.write_page(pid, &page).unwrap();
381        }
382
383        let _ = pool.fetch_page(p1, &mut pm).unwrap();
384        let _ = pool.fetch_page(p2, &mut pm).unwrap();
385
386        let mut mod1 = Page::zeroed();
387        mod1.data[0] = 0xAA;
388        pool.update_page(p1, &mod1).unwrap();
389
390        let mut mod2 = Page::zeroed();
391        mod2.data[0] = 0xBB;
392        pool.update_page(p2, &mod2).unwrap();
393
394        pool.unpin_page(p1, true).unwrap();
395        pool.unpin_page(p2, true).unwrap();
396
397        pool.flush_all(&mut pm).unwrap();
398
399        let d1 = pm.read_page(p1).unwrap();
400        let d2 = pm.read_page(p2).unwrap();
401        assert_eq!(d1.data[0], 0xAA);
402        assert_eq!(d2.data[0], 0xBB);
403    }
404}