Skip to main content

oxirs_tdb/storage/
buffer_pool.rs

1//! Buffer pool with LRU caching
2//!
3//! This module implements an in-memory buffer pool for caching disk pages
4//! with LRU (Least Recently Used) eviction policy.
5
6use crate::error::{Result, TdbError};
7use crate::storage::file_manager::FileManager;
8use crate::storage::page::{Page, PageId, PageType};
9use dashmap::DashMap;
10use parking_lot::{Mutex, RwLock};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::Arc;
13
14/// Frame ID within the buffer pool
15pub type FrameId = usize;
16
17/// Where the contents of a freshly-loaded frame come from.
18enum LoadSource {
19    /// Read an existing page from disk.
20    Disk,
21    /// Initialise a brand-new (just-allocated) page in memory.
22    New(PageType),
23}
24
25/// A buffer frame holding a cached page
26struct BufferFrame {
27    /// The cached page (None if frame is empty)
28    page: RwLock<Option<Page>>,
29    /// Access counter for LRU (higher = more recently used)
30    access_count: AtomicU64,
31    /// Whether this frame is currently pinned
32    pin_count: AtomicU64,
33}
34
35impl BufferFrame {
36    fn new() -> Self {
37        Self {
38            page: RwLock::new(None),
39            access_count: AtomicU64::new(0),
40            pin_count: AtomicU64::new(0),
41        }
42    }
43
44    fn is_pinned(&self) -> bool {
45        self.pin_count.load(Ordering::Acquire) > 0
46    }
47
48    fn pin(&self) {
49        self.pin_count.fetch_add(1, Ordering::AcqRel);
50    }
51
52    fn unpin(&self) {
53        let prev = self.pin_count.fetch_sub(1, Ordering::AcqRel);
54        debug_assert!(prev > 0, "Unpin called on unpinned frame");
55    }
56
57    fn access(&self) {
58        self.access_count.fetch_add(1, Ordering::AcqRel);
59    }
60
61    fn get_access_count(&self) -> u64 {
62        self.access_count.load(Ordering::Acquire)
63    }
64}
65
66/// Buffer pool for caching pages in memory
67///
68/// The buffer pool maintains a fixed-size cache of pages loaded from disk.
69/// When the cache is full, it uses LRU eviction to make room for new pages.
70pub struct BufferPool {
71    /// Fixed-size array of buffer frames
72    frames: Vec<BufferFrame>,
73    /// Map from PageId to FrameId
74    page_table: DashMap<PageId, FrameId>,
75    /// File manager for disk I/O
76    file_manager: Arc<FileManager>,
77    /// Next victim for eviction (clock hand)
78    clock_hand: AtomicU64,
79    /// Serializes the miss path (victim selection + eviction + load) so two
80    /// concurrent misses can never select and evict the same frame — the root
81    /// cause of the previous cross-page corruption (a TOCTOU between victim
82    /// selection and pinning). Cache hits do not take this lock.
83    load_lock: Mutex<()>,
84    /// Statistics
85    stats: BufferPoolStats,
86}
87
88/// Buffer pool statistics
89#[derive(Debug, Default)]
90pub struct BufferPoolStats {
91    /// Total number of page fetches
92    pub total_fetches: AtomicU64,
93    /// Number of cache hits
94    pub cache_hits: AtomicU64,
95    /// Number of cache misses
96    pub cache_misses: AtomicU64,
97    /// Number of page evictions
98    pub evictions: AtomicU64,
99    /// Number of pages written to disk
100    pub writes: AtomicU64,
101}
102
103impl Clone for BufferPoolStats {
104    fn clone(&self) -> Self {
105        Self {
106            total_fetches: AtomicU64::new(self.total_fetches.load(Ordering::Relaxed)),
107            cache_hits: AtomicU64::new(self.cache_hits.load(Ordering::Relaxed)),
108            cache_misses: AtomicU64::new(self.cache_misses.load(Ordering::Relaxed)),
109            evictions: AtomicU64::new(self.evictions.load(Ordering::Relaxed)),
110            writes: AtomicU64::new(self.writes.load(Ordering::Relaxed)),
111        }
112    }
113}
114
115impl BufferPoolStats {
116    /// Get cache hit rate (0.0 to 1.0)
117    pub fn hit_rate(&self) -> f64 {
118        let total = self.total_fetches.load(Ordering::Relaxed);
119        if total == 0 {
120            0.0
121        } else {
122            let hits = self.cache_hits.load(Ordering::Relaxed);
123            hits as f64 / total as f64
124        }
125    }
126
127    /// Reset statistics
128    pub fn reset(&self) {
129        self.total_fetches.store(0, Ordering::Relaxed);
130        self.cache_hits.store(0, Ordering::Relaxed);
131        self.cache_misses.store(0, Ordering::Relaxed);
132        self.evictions.store(0, Ordering::Relaxed);
133        self.writes.store(0, Ordering::Relaxed);
134    }
135
136    /// Clone statistics (manual implementation)
137    pub fn snapshot(&self) -> Self {
138        Self {
139            total_fetches: AtomicU64::new(self.total_fetches.load(Ordering::Relaxed)),
140            cache_hits: AtomicU64::new(self.cache_hits.load(Ordering::Relaxed)),
141            cache_misses: AtomicU64::new(self.cache_misses.load(Ordering::Relaxed)),
142            evictions: AtomicU64::new(self.evictions.load(Ordering::Relaxed)),
143            writes: AtomicU64::new(self.writes.load(Ordering::Relaxed)),
144        }
145    }
146}
147
148impl BufferPool {
149    /// Create a new buffer pool
150    pub fn new(pool_size: usize, file_manager: Arc<FileManager>) -> Self {
151        let mut frames = Vec::with_capacity(pool_size);
152        for _ in 0..pool_size {
153            frames.push(BufferFrame::new());
154        }
155
156        Self {
157            frames,
158            page_table: DashMap::new(),
159            file_manager,
160            clock_hand: AtomicU64::new(0),
161            load_lock: Mutex::new(()),
162            stats: BufferPoolStats::default(),
163        }
164    }
165
166    /// Access the underlying file manager (used by the store to persist the
167    /// superblock directly, outside the cached page space).
168    pub fn file_manager(&self) -> &Arc<FileManager> {
169        &self.file_manager
170    }
171
172    /// Fetch a page (from cache or disk)
173    pub fn fetch_page(&self, page_id: PageId) -> Result<PageGuard<'_>> {
174        self.stats.total_fetches.fetch_add(1, Ordering::Relaxed);
175
176        // Fast path: page already cached. `try_hit` validates the frame's
177        // identity under its latch and pins it, so it cannot return a guard to
178        // a frame that a concurrent eviction has re-assigned.
179        if let Some(guard) = self.try_hit(page_id) {
180            self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
181            return Ok(guard);
182        }
183
184        // Miss: serialize the load so two misses never race on the same victim.
185        let _load = self.load_lock.lock();
186
187        // Double-check under the load lock — another thread may have loaded the
188        // page while we waited.
189        if let Some(guard) = self.try_hit(page_id) {
190            self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
191            return Ok(guard);
192        }
193
194        self.stats.cache_misses.fetch_add(1, Ordering::Relaxed);
195        let frame_id = self.evict_and_load(page_id, LoadSource::Disk)?;
196        Ok(PageGuard {
197            frame_id,
198            page_id,
199            buffer_pool: self,
200        })
201    }
202
203    /// Create a new page
204    pub fn new_page(&self, page_type: PageType) -> Result<PageGuard<'_>> {
205        // Serialize with the miss path: allocation + frame claiming must be
206        // atomic with respect to concurrent evictions.
207        let _load = self.load_lock.lock();
208        let page_id = self.file_manager.allocate_page()?;
209        let frame_id = self.evict_and_load(page_id, LoadSource::New(page_type))?;
210        Ok(PageGuard {
211            frame_id,
212            page_id,
213            buffer_pool: self,
214        })
215    }
216
217    /// Fast cache-hit path.
218    ///
219    /// Returns a pinned [`PageGuard`] only if `page_id` is resident and the
220    /// frame still holds it. The pin is taken while holding the frame's read
221    /// latch, which is mutually exclusive with the write latch that
222    /// [`Self::evict_and_load`] holds across an eviction, so a hit can never be
223    /// granted on a frame mid-reassignment.
224    fn try_hit(&self, page_id: PageId) -> Option<PageGuard<'_>> {
225        let frame_id = *self.page_table.get(&page_id)?;
226        let frame = &self.frames[frame_id];
227
228        // Latch the frame's identity, then verify it before pinning.
229        let read = frame.page.read();
230        let holds_page = read.as_ref().map(|p| p.page_id()) == Some(page_id);
231        let still_mapped = self.page_table.get(&page_id).map(|e| *e) == Some(frame_id);
232        if holds_page && still_mapped {
233            frame.pin();
234            frame.access();
235            drop(read);
236            Some(PageGuard {
237                frame_id,
238                page_id,
239                buffer_pool: self,
240            })
241        } else {
242            None
243        }
244    }
245
246    /// Select a victim frame, evict its resident page (writing it back if
247    /// dirty), load `page_id` into it, register it in the page table, and pin
248    /// it — all while holding the frame's write latch so the frame's identity
249    /// change is atomic. Returns the pinned frame id.
250    ///
251    /// Must be called with `load_lock` held.
252    fn evict_and_load(&self, page_id: PageId, source: LoadSource) -> Result<FrameId> {
253        let pool_size = self.frames.len();
254        let mut checked = 0usize;
255        let max_checks = pool_size * 4; // enough sweeps to give every frame a second chance
256
257        loop {
258            if checked >= max_checks {
259                return Err(TdbError::BufferPoolFull);
260            }
261
262            let hand = self.clock_hand.fetch_add(1, Ordering::AcqRel) as usize % pool_size;
263            let frame = &self.frames[hand];
264
265            // Cheap pre-check: skip pinned frames without latching.
266            if frame.is_pinned() {
267                checked += 1;
268                continue;
269            }
270
271            // Latch the frame exclusively; this excludes `try_hit` readers.
272            let mut page_guard = frame.page.write();
273
274            // A concurrent hit may have pinned the frame between the pre-check
275            // and acquiring the latch — re-check under the latch.
276            if frame.is_pinned() {
277                drop(page_guard);
278                checked += 1;
279                continue;
280            }
281
282            if page_guard.is_some() {
283                // Second-chance: skip recently-accessed occupied frames.
284                if frame.get_access_count() > 0 {
285                    frame.access_count.fetch_sub(1, Ordering::AcqRel);
286                    drop(page_guard);
287                    checked += 1;
288                    continue;
289                }
290
291                // Evict the resident page.
292                let is_dirty = page_guard.as_ref().map(|p| p.is_dirty()).unwrap_or(false);
293                let old_id = page_guard.as_ref().map(|p| p.page_id());
294                if is_dirty {
295                    if let Some(mut old_page) = page_guard.take() {
296                        self.file_manager.write_page(&mut old_page)?;
297                        self.stats.writes.fetch_add(1, Ordering::Relaxed);
298                    }
299                } else {
300                    *page_guard = None;
301                }
302                if let Some(old_id) = old_id {
303                    self.page_table.remove(&old_id);
304                }
305                self.stats.evictions.fetch_add(1, Ordering::Relaxed);
306            }
307
308            // Install the new page content.
309            let new_page = match source {
310                LoadSource::Disk => self.file_manager.read_page(page_id)?,
311                LoadSource::New(page_type) => Page::new(page_id, page_type),
312            };
313            *page_guard = Some(new_page);
314
315            // Register + pin BEFORE releasing the latch so no concurrent
316            // eviction or hit can observe an inconsistent (mapped-but-unpinned
317            // or reassigned) state.
318            self.page_table.insert(page_id, hand);
319            frame.pin();
320            frame.access();
321            drop(page_guard);
322            return Ok(hand);
323        }
324    }
325
326    /// Flush a specific page to disk
327    pub fn flush_page(&self, page_id: PageId) -> Result<()> {
328        if let Some(frame_entry) = self.page_table.get(&page_id) {
329            let frame_id = *frame_entry;
330            let frame = &self.frames[frame_id];
331            let mut page_guard = frame.page.write();
332
333            if let Some(page) = page_guard.as_mut() {
334                if page.is_dirty() {
335                    self.file_manager.write_page(page)?;
336                    self.stats.writes.fetch_add(1, Ordering::Relaxed);
337                }
338            }
339        }
340        Ok(())
341    }
342
343    /// Flush all dirty pages to disk
344    pub fn flush_all(&self) -> Result<()> {
345        for entry in self.page_table.iter() {
346            let frame_id = *entry.value();
347            let frame = &self.frames[frame_id];
348            let mut page_guard = frame.page.write();
349
350            if let Some(page) = page_guard.as_mut() {
351                if page.is_dirty() {
352                    self.file_manager.write_page(page)?;
353                    self.stats.writes.fetch_add(1, Ordering::Relaxed);
354                }
355            }
356        }
357        self.file_manager.flush()?;
358        Ok(())
359    }
360
361    /// Drop a page from the cache WITHOUT writing it back to disk.
362    ///
363    /// This is required before a page is returned to the file manager's free
364    /// list (see [`crate::storage::FileManager::free_page`]): `free_page` writes a
365    /// free-list node directly through the file manager, bypassing the buffer
366    /// pool. If a stale (possibly dirty) copy of that page remained cached, a
367    /// later [`Self::flush_all`] would write it back over the free-list node,
368    /// corrupting the free list. Discarding the cached frame first closes that
369    /// cache-coherence hazard.
370    ///
371    /// The caller must ensure the page is no longer pinned (no live
372    /// [`PageGuard`] to it); a pinned frame is left in place and `false` is
373    /// returned so the caller can detect the misuse. Returns `true` when the page
374    /// was resident and has been evicted (or was not cached at all).
375    pub fn discard_page(&self, page_id: PageId) -> bool {
376        // Serialize with the miss/eviction path so we never race a concurrent
377        // load that is re-assigning a frame.
378        let _load = self.load_lock.lock();
379
380        let frame_id = match self.page_table.get(&page_id).map(|e| *e) {
381            Some(fid) => fid,
382            None => return true, // not cached: nothing to do
383        };
384
385        let frame = &self.frames[frame_id];
386        if frame.is_pinned() {
387            // A live guard still references this page; refuse to discard.
388            return false;
389        }
390
391        let mut page_guard = frame.page.write();
392        // Re-check identity under the latch.
393        if page_guard.as_ref().map(|p| p.page_id()) == Some(page_id) {
394            *page_guard = None;
395            frame.access_count.store(0, Ordering::Release);
396            self.page_table.remove(&page_id);
397        }
398        true
399    }
400
401    /// Unpin a page
402    fn unpin_page(&self, frame_id: FrameId) {
403        self.frames[frame_id].unpin();
404    }
405
406    /// Get buffer pool statistics
407    pub fn stats(&self) -> BufferPoolStats {
408        self.stats.snapshot()
409    }
410
411    /// Get pool size
412    pub fn pool_size(&self) -> usize {
413        self.frames.len()
414    }
415
416    /// Get number of cached pages
417    pub fn cached_pages(&self) -> usize {
418        self.page_table.len()
419    }
420}
421
422/// RAII guard for a pinned page
423///
424/// Automatically unpins the page when dropped.
425pub struct PageGuard<'a> {
426    frame_id: FrameId,
427    page_id: PageId,
428    buffer_pool: &'a BufferPool,
429}
430
431impl<'a> PageGuard<'a> {
432    /// Get immutable reference to the page.
433    ///
434    /// In debug builds this asserts the frame still holds the page this guard
435    /// was created for; for a fallible, always-on identity check use
436    /// [`PageGuard::page_checked`] (which the B+Tree hot path uses).
437    pub fn page(&self) -> parking_lot::RwLockReadGuard<'_, Option<Page>> {
438        let guard = self.buffer_pool.frames[self.frame_id].page.read();
439        debug_assert!(
440            guard.as_ref().map(|p| p.page_id()) == Some(self.page_id),
441            "PageGuard identity mismatch on frame {}: expected page {}",
442            self.frame_id,
443            self.page_id
444        );
445        guard
446    }
447
448    /// Get mutable reference to the page (see [`PageGuard::page`]).
449    pub fn page_mut(&self) -> parking_lot::RwLockWriteGuard<'_, Option<Page>> {
450        let guard = self.buffer_pool.frames[self.frame_id].page.write();
451        debug_assert!(
452            guard.as_ref().map(|p| p.page_id()) == Some(self.page_id),
453            "PageGuard identity mismatch on frame {}: expected page {}",
454            self.frame_id,
455            self.page_id
456        );
457        guard
458    }
459
460    /// Get an immutable reference to the page, verifying that the frame still
461    /// holds the expected page id.
462    ///
463    /// Returns [`TdbError::PageIdMismatch`] if the buffer frame has been
464    /// re-assigned to a different page, turning what would otherwise be a
465    /// silent wrong-page read into a detectable error.
466    pub fn page_checked(&self) -> Result<parking_lot::RwLockReadGuard<'_, Option<Page>>> {
467        let guard = self.buffer_pool.frames[self.frame_id].page.read();
468        match guard.as_ref().map(|p| p.page_id()) {
469            Some(actual) if actual == self.page_id => Ok(guard),
470            Some(actual) => Err(TdbError::PageIdMismatch {
471                expected: self.page_id,
472                actual,
473            }),
474            None => Err(TdbError::PageNotFound(self.page_id)),
475        }
476    }
477
478    /// Get a mutable reference to the page, verifying that the frame still
479    /// holds the expected page id (see [`PageGuard::page_checked`]).
480    pub fn page_mut_checked(&self) -> Result<parking_lot::RwLockWriteGuard<'_, Option<Page>>> {
481        let guard = self.buffer_pool.frames[self.frame_id].page.write();
482        match guard.as_ref().map(|p| p.page_id()) {
483            Some(actual) if actual == self.page_id => Ok(guard),
484            Some(actual) => Err(TdbError::PageIdMismatch {
485                expected: self.page_id,
486                actual,
487            }),
488            None => Err(TdbError::PageNotFound(self.page_id)),
489        }
490    }
491
492    /// Get page ID
493    pub fn page_id(&self) -> PageId {
494        self.page_id
495    }
496}
497
498impl<'a> Drop for PageGuard<'a> {
499    fn drop(&mut self) {
500        self.buffer_pool.unpin_page(self.frame_id);
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use tempfile::NamedTempFile;
508
509    #[test]
510    fn test_buffer_pool_creation() {
511        let temp_file = NamedTempFile::new().unwrap();
512        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
513        let bp = BufferPool::new(10, fm);
514
515        assert_eq!(bp.pool_size(), 10);
516        assert_eq!(bp.cached_pages(), 0);
517    }
518
519    #[test]
520    fn test_buffer_pool_new_page() {
521        let temp_file = NamedTempFile::new().unwrap();
522        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
523        let bp = BufferPool::new(10, fm);
524
525        let guard = bp.new_page(PageType::BTreeLeaf).unwrap();
526        assert_eq!(guard.page_id(), 0);
527        assert_eq!(bp.cached_pages(), 1);
528    }
529
530    #[test]
531    fn test_buffer_pool_fetch_page() {
532        let temp_file = NamedTempFile::new().unwrap();
533        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
534        let bp = BufferPool::new(10, fm.clone());
535
536        // Create and write a page
537        let page_id = {
538            let guard = bp.new_page(PageType::BTreeLeaf).unwrap();
539            let mut page = guard.page_mut();
540            page.as_mut().unwrap().write_at(0, b"test data").unwrap();
541            guard.page_id()
542        };
543
544        // Flush to disk
545        bp.flush_page(page_id).unwrap();
546
547        // Fetch the page
548        let guard = bp.fetch_page(page_id).unwrap();
549        let page = guard.page();
550        let data = page.as_ref().unwrap().read_at(0, 9).unwrap();
551        assert_eq!(data, b"test data");
552    }
553
554    #[test]
555    fn test_buffer_pool_cache_hit() {
556        let temp_file = NamedTempFile::new().unwrap();
557        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
558        let bp = BufferPool::new(10, fm);
559
560        let guard1 = bp.new_page(PageType::BTreeLeaf).unwrap();
561        let page_id = guard1.page_id();
562        drop(guard1);
563
564        // Fetch same page - should be cache hit
565        let _guard2 = bp.fetch_page(page_id).unwrap();
566
567        let stats = bp.stats();
568        assert!(stats.cache_hits.load(Ordering::Relaxed) > 0);
569    }
570
571    #[test]
572    fn test_buffer_pool_eviction() {
573        let temp_file = NamedTempFile::new().unwrap();
574        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
575        let bp = BufferPool::new(3, fm); // Small pool
576
577        // Fill the pool
578        let _g1 = bp.new_page(PageType::BTreeLeaf).unwrap();
579        let _g2 = bp.new_page(PageType::BTreeLeaf).unwrap();
580        let _g3 = bp.new_page(PageType::BTreeLeaf).unwrap();
581
582        assert_eq!(bp.cached_pages(), 3);
583
584        // Drop guards to allow eviction
585        drop(_g1);
586        drop(_g2);
587        drop(_g3);
588
589        // Allocate one more - should trigger eviction
590        let _g4 = bp.new_page(PageType::BTreeLeaf).unwrap();
591
592        let stats = bp.stats();
593        assert!(stats.evictions.load(Ordering::Relaxed) > 0);
594    }
595
596    #[test]
597    fn test_buffer_pool_flush_all() {
598        let temp_file = NamedTempFile::new().unwrap();
599        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
600        let bp = BufferPool::new(10, fm);
601
602        // Create some dirty pages by writing data to them
603        {
604            let g1 = bp.new_page(PageType::BTreeLeaf).unwrap();
605            let mut page1 = g1.page_mut();
606            page1.as_mut().unwrap().write_at(0, b"data1").unwrap();
607        }
608        {
609            let g2 = bp.new_page(PageType::BTreeLeaf).unwrap();
610            let mut page2 = g2.page_mut();
611            page2.as_mut().unwrap().write_at(0, b"data2").unwrap();
612        }
613
614        bp.flush_all().unwrap();
615
616        let stats = bp.stats();
617        assert!(stats.writes.load(Ordering::Relaxed) >= 2);
618    }
619
620    #[test]
621    fn test_buffer_pool_hit_rate() {
622        let temp_file = NamedTempFile::new().unwrap();
623        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
624        let bp = BufferPool::new(10, fm);
625
626        let guard = bp.new_page(PageType::BTreeLeaf).unwrap();
627        let page_id = guard.page_id();
628        drop(guard);
629
630        // Multiple fetches of same page
631        for _ in 0..5 {
632            let _g = bp.fetch_page(page_id).unwrap();
633        }
634
635        let stats = bp.stats();
636        assert!(stats.hit_rate() > 0.8); // Should be high
637    }
638
639    #[test]
640    fn test_buffer_pool_pin_prevents_eviction() {
641        let temp_file = NamedTempFile::new().unwrap();
642        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
643        let bp = BufferPool::new(2, fm); // Very small pool
644
645        let g1 = bp.new_page(PageType::BTreeLeaf).unwrap();
646        let page_id1 = g1.page_id();
647        // Keep g1 pinned
648
649        let g2 = bp.new_page(PageType::BTreeLeaf).unwrap();
650        drop(g2);
651
652        // Try to allocate one more - should not evict pinned page
653        let result = bp.new_page(PageType::BTreeLeaf);
654
655        // Should succeed (evicts g2, not g1)
656        assert!(result.is_ok());
657
658        // g1 should still be cached
659        assert!(bp.page_table.contains_key(&page_id1));
660    }
661
662    #[test]
663    fn test_page_guard_checked_matches_identity() {
664        let temp_file = NamedTempFile::new().unwrap();
665        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
666        let bp = BufferPool::new(10, fm);
667
668        let guard = bp.new_page(PageType::BTreeLeaf).unwrap();
669        // A guard over a resident, correctly-mapped frame validates cleanly.
670        // Each temporary guard is dropped at the end of its statement, so the
671        // read latch is released before the write latch is requested.
672        assert!(guard.page_checked().is_ok());
673        assert!(guard.page_mut_checked().is_ok());
674    }
675
676    #[test]
677    fn test_buffer_pool_concurrent_no_cross_page_corruption() {
678        use std::thread;
679
680        let temp_file = NamedTempFile::new().unwrap();
681        let fm = Arc::new(FileManager::open(temp_file.path(), false).unwrap());
682        // Deliberately tiny pool vs. page count to force heavy eviction and
683        // exercise the miss/eviction race that previously corrupted frames.
684        let bp = Arc::new(BufferPool::new(8, fm));
685        let num_pages: u64 = 64;
686
687        // Stamp each page's own id into its first 8 bytes.
688        for _ in 0..num_pages {
689            let guard = bp.new_page(PageType::BTreeLeaf).unwrap();
690            let pid = guard.page_id();
691            {
692                let mut page = guard.page_mut();
693                page.as_mut()
694                    .unwrap()
695                    .write_at(0, &pid.to_le_bytes())
696                    .unwrap();
697            }
698        }
699        bp.flush_all().unwrap();
700
701        let mut handles = Vec::new();
702        for _ in 0..8 {
703            let bp = bp.clone();
704            handles.push(thread::spawn(move || {
705                for i in 0..1000u64 {
706                    let pid = i % num_pages;
707                    let guard = bp.fetch_page(pid).expect("fetch");
708                    let page_lock = guard.page_checked().expect("frame identity");
709                    let page = page_lock.as_ref().expect("resident");
710                    let bytes = page.read_at(0, 8).expect("read");
711                    let mut arr = [0u8; 8];
712                    arr.copy_from_slice(bytes);
713                    assert_eq!(
714                        u64::from_le_bytes(arr),
715                        pid,
716                        "buffer frame returned bytes belonging to a different page"
717                    );
718                }
719            }));
720        }
721        for h in handles {
722            h.join().expect("thread panicked (corruption detected)");
723        }
724    }
725}