use std::collections::HashMap;
use cypherlite_core::{CypherLiteError, PageId, Result};
use super::PAGE_SIZE;
const HEAD: usize = 0;
const TAIL: usize = 1;
struct LruNode {
prev: usize,
next: usize,
page_id: PageId, }
struct LruList {
arena: Vec<LruNode>,
map: HashMap<PageId, usize>,
free_slots: Vec<usize>,
}
impl LruList {
fn new(capacity: usize) -> Self {
let sentinel_id = PageId(u32::MAX);
let mut arena = Vec::with_capacity(capacity + 2);
arena.push(LruNode {
prev: HEAD,
next: TAIL,
page_id: sentinel_id,
});
arena.push(LruNode {
prev: HEAD,
next: TAIL,
page_id: sentinel_id,
});
Self {
arena,
map: HashMap::with_capacity(capacity),
free_slots: Vec::new(),
}
}
fn push_back(&mut self, page_id: PageId) {
debug_assert!(
!self.map.contains_key(&page_id),
"push_back: duplicate page_id"
);
let idx = self.alloc_node(page_id);
self.link_before(TAIL, idx);
self.map.insert(page_id, idx);
}
fn remove(&mut self, page_id: PageId) {
if let Some(idx) = self.map.remove(&page_id) {
self.unlink(idx);
self.free_slots.push(idx);
}
}
fn touch(&mut self, page_id: PageId) {
if let Some(&idx) = self.map.get(&page_id) {
self.unlink(idx);
self.link_before(TAIL, idx);
}
}
fn iter_lru(&self) -> LruIter<'_> {
LruIter {
list: self,
current: self.arena[HEAD].next,
}
}
fn alloc_node(&mut self, page_id: PageId) -> usize {
if let Some(idx) = self.free_slots.pop() {
self.arena[idx].page_id = page_id;
self.arena[idx].prev = 0;
self.arena[idx].next = 0;
idx
} else {
let idx = self.arena.len();
self.arena.push(LruNode {
prev: 0,
next: 0,
page_id,
});
idx
}
}
fn link_before(&mut self, before: usize, idx: usize) {
let prev = self.arena[before].prev;
self.arena[idx].prev = prev;
self.arena[idx].next = before;
self.arena[prev].next = idx;
self.arena[before].prev = idx;
}
fn unlink(&mut self, idx: usize) {
let prev = self.arena[idx].prev;
let nxt = self.arena[idx].next;
self.arena[prev].next = nxt;
self.arena[nxt].prev = prev;
}
}
struct LruIter<'a> {
list: &'a LruList,
current: usize,
}
impl<'a> Iterator for LruIter<'a> {
type Item = PageId;
fn next(&mut self) -> Option<PageId> {
if self.current == TAIL {
return None;
}
let node = &self.list.arena[self.current];
let page_id = node.page_id;
self.current = node.next;
Some(page_id)
}
}
#[derive(Debug)]
struct BufferFrame {
page_id: PageId,
data: [u8; PAGE_SIZE],
dirty: bool,
pin_count: u32,
}
pub struct BufferPool {
frames: HashMap<PageId, usize>, pool: Vec<BufferFrame>,
lru: LruList,
capacity: usize,
}
impl BufferPool {
pub fn new(capacity: usize) -> Self {
Self {
frames: HashMap::with_capacity(capacity),
pool: Vec::with_capacity(capacity),
lru: LruList::new(capacity),
capacity,
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn size(&self) -> usize {
self.pool.len()
}
pub fn insert(&mut self, page_id: PageId, data: [u8; PAGE_SIZE]) {
if let Some(&idx) = self.frames.get(&page_id) {
self.pool[idx].data = data;
self.lru.touch(page_id);
} else {
let idx = self.pool.len();
self.pool.push(BufferFrame {
page_id,
data,
dirty: false,
pin_count: 0,
});
self.frames.insert(page_id, idx);
self.lru.push_back(page_id);
}
}
pub fn get(&mut self, page_id: PageId) -> Option<&[u8; PAGE_SIZE]> {
if let Some(&idx) = self.frames.get(&page_id) {
self.lru.touch(page_id);
Some(&self.pool[idx].data)
} else {
None
}
}
pub fn get_mut(&mut self, page_id: PageId) -> Option<&mut [u8; PAGE_SIZE]> {
if let Some(&idx) = self.frames.get(&page_id) {
self.lru.touch(page_id);
self.pool[idx].dirty = true;
Some(&mut self.pool[idx].data)
} else {
None
}
}
pub fn pin(&mut self, page_id: PageId) {
if let Some(&idx) = self.frames.get(&page_id) {
self.pool[idx].pin_count += 1;
}
}
pub fn unpin(&mut self, page_id: PageId) {
if let Some(&idx) = self.frames.get(&page_id) {
if self.pool[idx].pin_count > 0 {
self.pool[idx].pin_count -= 1;
}
}
}
pub fn mark_dirty(&mut self, page_id: PageId) {
if let Some(&idx) = self.frames.get(&page_id) {
self.pool[idx].dirty = true;
}
}
pub fn is_dirty(&self, page_id: PageId) -> bool {
self.frames
.get(&page_id)
.map(|&idx| self.pool[idx].dirty)
.unwrap_or(false)
}
pub fn is_pinned(&self, page_id: PageId) -> bool {
self.frames
.get(&page_id)
.map(|&idx| self.pool[idx].pin_count > 0)
.unwrap_or(false)
}
pub fn is_full(&self) -> bool {
self.pool.len() >= self.capacity
}
pub fn evict(&mut self) -> Result<Option<(PageId, [u8; PAGE_SIZE], bool)>> {
let evict_pid = self.lru.iter_lru().find(|pid| {
self.frames
.get(pid)
.map(|&idx| self.pool[idx].pin_count == 0)
.unwrap_or(false)
});
match evict_pid {
Some(page_id) => {
self.lru.remove(page_id);
let idx = self.frames.remove(&page_id).expect("frame index");
let frame = self.pool.swap_remove(idx);
self.fix_swapped_frame(idx);
Ok(Some((frame.page_id, frame.data, frame.dirty)))
}
None if self.pool.is_empty() => Ok(None),
None => Err(CypherLiteError::OutOfSpace),
}
}
pub fn remove(&mut self, page_id: PageId) -> Option<([u8; PAGE_SIZE], bool)> {
if let Some(idx) = self.frames.remove(&page_id) {
let frame = self.pool.swap_remove(idx);
self.fix_swapped_frame(idx);
self.lru.remove(page_id);
Some((frame.data, frame.dirty))
} else {
None
}
}
pub fn clear_dirty(&mut self, page_id: PageId) {
if let Some(&idx) = self.frames.get(&page_id) {
self.pool[idx].dirty = false;
}
}
fn fix_swapped_frame(&mut self, idx: usize) {
if idx < self.pool.len() {
let swapped_page_id = self.pool[idx].page_id;
self.frames.insert(swapped_page_id, idx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_pool_creation() {
let pool = BufferPool::new(256);
assert_eq!(pool.capacity(), 256);
assert_eq!(pool.size(), 0);
}
#[test]
fn test_insert_and_fetch() {
let mut pool = BufferPool::new(10);
let mut data = [0u8; PAGE_SIZE];
data[0] = 0xAB;
pool.insert(PageId(2), data);
let fetched = pool.get(PageId(2)).expect("should be cached");
assert_eq!(fetched[0], 0xAB);
}
#[test]
fn test_fetch_missing_page_returns_none() {
let mut pool = BufferPool::new(10);
assert!(pool.get(PageId(99)).is_none());
}
#[test]
fn test_mark_dirty() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
assert!(!pool.is_dirty(PageId(2)));
pool.mark_dirty(PageId(2));
assert!(pool.is_dirty(PageId(2)));
}
#[test]
fn test_pin_and_unpin() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
assert!(!pool.is_pinned(PageId(2)));
pool.pin(PageId(2));
assert!(pool.is_pinned(PageId(2)));
pool.unpin(PageId(2));
assert!(!pool.is_pinned(PageId(2)));
}
#[test]
fn test_evict_lru_unpinned() {
let mut pool = BufferPool::new(3);
pool.insert(PageId(2), [1u8; PAGE_SIZE]);
pool.insert(PageId(3), [2u8; PAGE_SIZE]);
pool.insert(PageId(4), [3u8; PAGE_SIZE]);
let evicted = pool.evict().expect("evict").expect("some page");
assert_eq!(evicted.0, PageId(2));
assert_eq!(evicted.1[0], 1);
assert!(!evicted.2); }
#[test]
fn test_evict_skips_pinned() {
let mut pool = BufferPool::new(3);
pool.insert(PageId(2), [1u8; PAGE_SIZE]);
pool.insert(PageId(3), [2u8; PAGE_SIZE]);
pool.insert(PageId(4), [3u8; PAGE_SIZE]);
pool.pin(PageId(2));
let evicted = pool.evict().expect("evict").expect("some page");
assert_eq!(evicted.0, PageId(3));
}
#[test]
fn test_evict_all_pinned_returns_error() {
let mut pool = BufferPool::new(2);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
pool.pin(PageId(2));
pool.pin(PageId(3));
let result = pool.evict();
assert!(matches!(result, Err(CypherLiteError::OutOfSpace)));
}
#[test]
fn test_evict_dirty_page_reports_dirty() {
let mut pool = BufferPool::new(2);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
pool.mark_dirty(PageId(2));
let evicted = pool.evict().expect("evict").expect("some page");
assert_eq!(evicted.0, PageId(2));
assert!(evicted.2); }
#[test]
fn test_get_mut_marks_dirty() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
assert!(!pool.is_dirty(PageId(2)));
let data = pool.get_mut(PageId(2)).expect("get_mut");
data[0] = 0xFF;
assert!(pool.is_dirty(PageId(2)));
}
#[test]
fn test_lru_ordering_updated_on_access() {
let mut pool = BufferPool::new(3);
pool.insert(PageId(2), [1u8; PAGE_SIZE]);
pool.insert(PageId(3), [2u8; PAGE_SIZE]);
pool.insert(PageId(4), [3u8; PAGE_SIZE]);
pool.get(PageId(2));
let evicted = pool.evict().expect("evict").expect("page");
assert_eq!(evicted.0, PageId(3));
}
#[test]
fn test_remove_page() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0xAB; PAGE_SIZE]);
assert_eq!(pool.size(), 1);
let removed = pool.remove(PageId(2));
assert!(removed.is_some());
assert_eq!(pool.size(), 0);
assert!(pool.get(PageId(2)).is_none());
}
#[test]
fn test_clear_dirty() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.mark_dirty(PageId(2));
assert!(pool.is_dirty(PageId(2)));
pool.clear_dirty(PageId(2));
assert!(!pool.is_dirty(PageId(2)));
}
#[test]
fn test_is_full() {
let mut pool = BufferPool::new(2);
assert!(!pool.is_full());
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
assert!(!pool.is_full());
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
assert!(pool.is_full());
}
#[test]
fn test_custom_capacity() {
let pool = BufferPool::new(1024);
assert_eq!(pool.capacity(), 1024);
}
#[test]
fn test_insert_update_existing() {
let mut pool = BufferPool::new(10);
pool.insert(PageId(2), [0xAA; PAGE_SIZE]);
pool.insert(PageId(2), [0xBB; PAGE_SIZE]); let data = pool.get(PageId(2)).expect("cached");
assert_eq!(data[0], 0xBB);
assert_eq!(pool.size(), 1); }
#[test]
fn test_evict_empty_pool_returns_none() {
let mut pool = BufferPool::new(10);
let result = pool.evict().expect("no error");
assert!(result.is_none());
}
#[test]
fn test_lru_touch_is_o1() {
let n: u32 = 1000;
let mut pool = BufferPool::new(n as usize + 1);
for i in 0..n {
pool.insert(PageId(i), [i as u8; PAGE_SIZE]);
}
for i in (0..n).rev() {
pool.get(PageId(i));
}
pool.get(PageId(999));
let evicted = pool.evict().expect("ok").expect("some page");
assert_eq!(evicted.0, PageId(998));
}
#[test]
fn test_consecutive_touch_no_duplicates() {
let mut pool = BufferPool::new(3);
pool.insert(PageId(1), [0u8; PAGE_SIZE]);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
pool.get(PageId(1));
pool.get(PageId(1));
pool.get(PageId(1));
let evicted = pool.evict().expect("ok").expect("some");
assert_eq!(evicted.0, PageId(2));
let evicted2 = pool.evict().expect("ok").expect("some");
assert_eq!(evicted2.0, PageId(3));
}
#[test]
fn test_cache_size_one_insert_evict() {
let mut pool = BufferPool::new(1);
pool.insert(PageId(10), [0xAA; PAGE_SIZE]);
assert_eq!(pool.size(), 1);
let evicted = pool.evict().expect("ok").expect("some");
assert_eq!(evicted.0, PageId(10));
assert_eq!(evicted.1[0], 0xAA);
assert_eq!(pool.size(), 0);
pool.insert(PageId(20), [0xBB; PAGE_SIZE]);
assert_eq!(pool.size(), 1);
let data = pool.get(PageId(20)).expect("cached");
assert_eq!(data[0], 0xBB);
}
#[test]
fn test_lru_ordering_mixed_operations() {
let mut pool = BufferPool::new(4);
pool.insert(PageId(1), [1u8; PAGE_SIZE]);
pool.insert(PageId(2), [2u8; PAGE_SIZE]);
pool.insert(PageId(3), [3u8; PAGE_SIZE]);
pool.insert(PageId(4), [4u8; PAGE_SIZE]);
pool.get(PageId(1)); pool.get_mut(PageId(3)); pool.insert(PageId(2), [0xBB; PAGE_SIZE]);
let evicted = pool.evict().expect("ok").expect("some");
assert_eq!(evicted.0, PageId(4));
let evicted2 = pool.evict().expect("ok").expect("some");
assert_eq!(evicted2.0, PageId(1));
}
#[test]
fn test_remove_maintains_lru_order() {
let mut pool = BufferPool::new(5);
pool.insert(PageId(1), [0u8; PAGE_SIZE]);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
pool.insert(PageId(4), [0u8; PAGE_SIZE]);
pool.remove(PageId(2));
let evicted = pool.evict().expect("ok").expect("some");
assert_eq!(evicted.0, PageId(1));
let evicted2 = pool.evict().expect("ok").expect("some");
assert_eq!(evicted2.0, PageId(3));
}
#[test]
fn test_evict_all_dirty_reports_dirty() {
let mut pool = BufferPool::new(3);
pool.insert(PageId(1), [0u8; PAGE_SIZE]);
pool.insert(PageId(2), [0u8; PAGE_SIZE]);
pool.insert(PageId(3), [0u8; PAGE_SIZE]);
pool.mark_dirty(PageId(1));
pool.mark_dirty(PageId(2));
pool.mark_dirty(PageId(3));
let evicted = pool.evict().expect("ok").expect("some");
assert_eq!(evicted.0, PageId(1));
assert!(evicted.2); }
}