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