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