use std::fmt;
use std::io::{Error, Result};
use std::path::Path;
use std::os::fd::AsRawFd;
use std::num::NonZeroUsize;
use std::collections::BTreeMap;
use log::{debug, warn};
use lru::LruCache;
use crate::{BlockIndex, BlockIndexIter};
use crate::buffer::DataBlock;
use crate::file::DirtyDataBlocks;
use super::Cache;
pub(crate) struct LocalDiskCache {
addr: u64, size: usize,
nslots: usize,
free_slots: Vec<u32>,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
file: std::fs::File,
closed: std::sync::atomic::AtomicBool,
pub(crate) data_blocks_cache: LruCache<BlockIndex, DataBlock>,
pub(crate) data_blocks_dirty: BTreeMap<BlockIndex, DataBlock>, pub(crate) data_cache_blocks: usize,
pub(crate) data_block_size: usize,
}
impl fmt::Display for LocalDiskCache {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, " data cache blocks limit: {}, data lru cache size: {}, data dirty size: {}, free slots: {}/{}",
self.data_cache_blocks, self.data_blocks_cache.len(), self.data_blocks_dirty.len(),
self.free_slots.len(), self.nslots)
}
}
impl Drop for LocalDiskCache {
fn drop(&mut self) {
if let Err(e) = self.teardown() {
warn!("local disk cache - teardown during drop failed: {}", e);
}
}
}
impl LocalDiskCache {
const DIRTY_SLOT_HEADROOM: usize = 1024;
fn pool_slots(data_cache_blocks: usize, max_dirty_blocks: usize) -> usize {
(data_cache_blocks + max_dirty_blocks + Self::DIRTY_SLOT_HEADROOM).max(1)
}
pub(crate) fn open_or_create(
file_path: impl AsRef<Path>,
max_dirty_blocks: usize,
data_cache_blocks: usize,
data_block_size: usize,
) -> Result<Self> {
let nslots = Self::pool_slots(data_cache_blocks, max_dirty_blocks);
let size = nslots * data_block_size;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(file_path)?;
let fd = file.as_raw_fd();
unsafe {
let ret = libc::ftruncate(fd, size as libc::off_t);
if ret != 0 {
return Err(Error::last_os_error());
}
}
let addr = unsafe {
libc::mmap(
std::ptr::null_mut::<libc::c_void>(),
size as libc::size_t,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
)
};
if addr == libc::MAP_FAILED {
return Err(Error::last_os_error());
}
let data_blocks_cache = lru::LruCache::new(
NonZeroUsize::new(data_cache_blocks).or(NonZeroUsize::new(1)).unwrap()
);
debug!("local disk cache - {} slots of {} bytes, {} bytes mapped",
nslots, data_block_size, size);
Ok(Self {
addr: addr as u64,
size,
nslots,
free_slots: (0..nslots as u32).rev().collect(),
file,
closed: std::sync::atomic::AtomicBool::new(false),
data_blocks_cache,
data_blocks_dirty: BTreeMap::new(),
data_cache_blocks,
data_block_size,
})
}
#[inline]
fn slot_offset(&self, slot: u32) -> usize {
slot as usize * self.data_block_size
}
fn slot_of(&self, block: &DataBlock) -> Option<u32> {
let p = block.as_slice().as_ptr() as usize;
let base = self.addr as usize;
if p < base || p >= base + self.size {
return None;
}
Some(((p - base) / self.data_block_size) as u32)
}
fn new_dirty_block(&mut self, blk_idx: BlockIndex) -> DataBlock {
let Some(slot) = self.free_slots.pop() else {
warn!("local disk cache - slot pool of {} exhausted, falling back to a heap block for index {}",
self.nslots, blk_idx);
let block = DataBlock::new_alloc(blk_idx, self.data_block_size);
block.set_dirty();
block.lock();
return block;
};
let ptr = unsafe {
(self.addr as *mut u8).add(self.slot_offset(slot))
};
let block = DataBlock::new_mmap(blk_idx, ptr, self.data_block_size);
block.as_mut_slice().fill(0);
block.set_dirty();
block.lock();
block
}
fn release(&mut self, block: &DataBlock) {
let Some(slot) = self.slot_of(block) else {
return; };
self.punch(slot);
self.free_slots.push(slot);
}
#[cfg(target_os = "linux")]
fn punch(&self, slot: u32) {
let fd = self.file.as_raw_fd();
let offset = self.slot_offset(slot) as libc::off_t;
let len = self.data_block_size as libc::off_t;
let ret = unsafe {
libc::fallocate(fd, libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE, offset, len)
};
if ret == -1 {
panic!("fallocate failed to punch hole at offset: {}, len: {}, error: {}",
offset, len, Error::last_os_error());
}
}
#[cfg(not(target_os = "linux"))]
fn punch(&self, slot: u32) {
let _ = slot;
}
fn teardown(&self) -> Result<()> {
use std::sync::atomic::Ordering;
if self.closed.swap(true, Ordering::AcqRel) {
return Ok(());
}
let ret = unsafe {
libc::msync(self.addr as *mut libc::c_void, self.size, libc::MS_SYNC | libc::MS_INVALIDATE)
};
if ret != 0 {
return Err(Error::last_os_error());
}
let ret = unsafe {
libc::munmap(self.addr as *mut libc::c_void, self.size)
};
if ret != 0 {
return Err(Error::last_os_error());
}
Ok(())
}
}
impl Cache for LocalDiskCache {
fn set_size(&self, _: usize) {
}
fn set_unlimited(&mut self) {
self.data_blocks_cache.resize(NonZeroUsize::new(usize::MAX).unwrap());
}
fn restore_limit(&mut self) {
self.data_blocks_cache.resize(
NonZeroUsize::new(self.data_cache_blocks).or(NonZeroUsize::new(1)).unwrap()
);
}
fn new_block(&self, blk_idx: BlockIndex) -> DataBlock {
DataBlock::new_alloc(blk_idx, self.data_block_size)
}
fn get(&mut self, blk_idx: &BlockIndex) -> Option<&DataBlock> {
if let Some(block) = self.data_blocks_dirty.get(blk_idx) {
debug!("Cache Hit on dirty list for block index: {}", blk_idx);
assert!(block.is_locked());
return Some(block);
}
if let Some(block) = (self.data_cache_blocks > 0).then(|| self.data_blocks_cache.get(blk_idx)).flatten() {
debug!("Cache Hit on cache list for block index: {}", blk_idx);
assert!(!block.is_locked());
block.lock();
return Some(block);
}
None
}
fn insert(&mut self, blk_idx: BlockIndex, block: DataBlock) -> Option<DataBlock> {
let _ = self.data_blocks_cache.pop(&blk_idx);
assert!(!block.is_dirty());
let mut dirty_block = self.new_dirty_block(blk_idx);
dirty_block.copy(0, block.as_slice());
let displaced = self.data_blocks_dirty.insert(blk_idx, dirty_block);
if let Some(old) = &displaced {
self.release(old);
}
displaced
}
fn has(&self, blk_idx: &BlockIndex) -> bool {
if self.data_blocks_dirty.contains_key(blk_idx) {
return true;
}
self.data_cache_blocks > 0 && self.data_blocks_cache.contains(blk_idx)
}
fn insert_clean(&mut self, blk_idx: BlockIndex, block: DataBlock) -> Option<DataBlock> {
if self.data_cache_blocks == 0 {
return Some(block);
}
debug_assert!(!block.is_dirty(), "insert_clean given a dirty block");
debug_assert!(!self.data_blocks_dirty.contains_key(&blk_idx));
let mut cached = self.new_dirty_block(blk_idx);
cached.copy(0, block.as_slice());
cached.set_should_cache();
cached.clear_dirty();
cached.unlock();
if let Some((old_blk_idx, old)) = self.data_blocks_cache.push(blk_idx, cached) {
if old_blk_idx == blk_idx {
panic!("block already exists, failed to insert clean block index {} into data blocks cache", blk_idx);
}
self.release(&old);
}
None
}
fn remove(&mut self, blk_idx: &BlockIndex) -> bool {
let mut removed = false;
if let Some(block) = self.data_blocks_cache.pop(&blk_idx) {
self.release(&block);
removed = true;
}
if let Some(block) = self.data_blocks_dirty.remove(blk_idx) {
block.clear_dirty();
block.unlock();
self.release(&block);
removed = true;
}
removed
}
fn contains(&mut self, blk_idx: &BlockIndex) -> bool {
if self.data_blocks_dirty.contains_key(blk_idx) {
return true;
}
if let Some(block) = (self.data_cache_blocks > 0).then(|| self.data_blocks_cache.pop(blk_idx)).flatten() {
block.lock();
self.data_blocks_dirty.insert(*blk_idx, block);
return true;
}
false
}
fn get_mut(&mut self, blk_idx: &BlockIndex) -> Option<&mut DataBlock> {
if self.data_blocks_dirty.contains_key(blk_idx) {
return self.data_blocks_dirty.get_mut(blk_idx);
}
if let Some(block) = (self.data_cache_blocks > 0).then(|| self.data_blocks_cache.pop(blk_idx)).flatten() {
block.lock();
block.set_dirty();
self.data_blocks_dirty.insert(*blk_idx, block);
return self.data_blocks_dirty.get_mut(blk_idx);
}
None
}
fn write_prepare(&mut self, off: usize, len: usize) -> Vec<BlockIndex> {
let mut output = Vec::new();
let blk_iter = BlockIndexIter::new(off, len, self.data_block_size);
debug!("start to write prepare for write offset {}, len {}", off, len);
for (blk_idx, start_off, data_len) in blk_iter {
if start_off == 0 && data_len == self.data_block_size {
let _ = self.data_blocks_cache.pop(&blk_idx);
continue;
}
if self.data_blocks_dirty.contains_key(&blk_idx) {
continue;
}
if let Some(block) = (self.data_cache_blocks > 0).then(|| self.data_blocks_cache.pop(&blk_idx)).flatten() {
block.lock();
block.set_dirty();
self.data_blocks_dirty.insert(blk_idx, block);
continue;
}
output.push(blk_idx);
}
debug!("end of write prepare {} of blocks need to be retrieve", output.len());
output
}
fn update_cache(&mut self, blk_idx: &BlockIndex, off: usize, buf: &[u8]) {
if let Some(block) = self.data_blocks_dirty.get_mut(blk_idx) {
block.copy(off, buf);
} else if let Some(mut block) = self.data_blocks_cache.pop(blk_idx) {
block.lock();
block.copy(off, buf);
block.set_dirty();
self.data_blocks_dirty.insert(*blk_idx, block);
warn!("update_cache - block index: {blk_idx} not in dirty list but in cache list, this is not by design");
} else {
let mut block = self.new_dirty_block(*blk_idx);
block.copy(off, buf);
self.data_blocks_dirty.insert(*blk_idx, block);
}
}
fn truncate_data_block(&mut self, blk_idx: &BlockIndex, offset_to_discard: usize) -> bool {
if let Some(block) = self.data_blocks_dirty.get_mut(&blk_idx) {
let buf = block.as_mut_slice();
let (_, to_clear) = buf.split_at_mut(offset_to_discard);
to_clear.fill(0);
debug!("data block in dirty list, data cleared");
return true;
}
if let Some(block) = (self.data_cache_blocks > 0).then(|| self.data_blocks_cache.pop(&blk_idx)).flatten() {
block.lock();
let buf = block.as_mut_slice();
let (_, to_clear) = buf.split_at_mut(offset_to_discard);
to_clear.fill(0);
debug!("data block in cache list, data cleared");
block.set_dirty();
self.data_blocks_dirty.insert(*blk_idx, block);
return true;
}
false
}
fn dirty_count(&self) -> usize {
self.data_blocks_dirty.len()
}
fn truncate_blocks_above(&mut self, boundary: BlockIndex) -> usize {
let mut removed = 0;
let dirty: Vec<BlockIndex> = self.data_blocks_dirty
.range(boundary..)
.map(|(k, _)| *k)
.collect();
for k in dirty {
if let Some(block) = self.data_blocks_dirty.remove(&k) {
removed += 1;
self.release(&block);
}
}
if self.data_cache_blocks > 0 {
let clean: Vec<BlockIndex> = self.data_blocks_cache
.iter()
.map(|(k, _)| *k)
.filter(|k| *k >= boundary)
.collect();
for k in clean {
if let Some(block) = self.data_blocks_cache.pop(&k) {
self.release(&block);
}
}
}
removed
}
fn get_dirty(&self) -> DirtyDataBlocks<'_> {
let b: BTreeMap<BlockIndex, &DataBlock> = self.data_blocks_dirty.iter()
.map(|(idx, blk)| (*idx, blk))
.collect();
DirtyDataBlocks { inner: Some(b), owned: None }
}
fn clear_dirty(&mut self) {
while let Some((blk_idx, block)) = self.data_blocks_dirty.pop_first() {
block.clear_dirty();
block.unlock();
if self.data_cache_blocks == 0 {
self.release(&block);
continue;
}
if let Some((old_blk_idx, old)) = self.data_blocks_cache.push(blk_idx, block) {
if old_blk_idx == blk_idx {
panic!("block already exists, failed to put back block index {} into data blocks cache", blk_idx);
}
self.release(&old);
}
}
}
fn demote_dirty(&mut self, indexes: &[BlockIndex]) {
for blk_idx in indexes {
let Some(block) = self.data_blocks_dirty.remove(blk_idx) else {
continue;
};
block.clear_dirty();
block.unlock();
if self.data_cache_blocks == 0 {
self.release(&block);
continue;
}
if let Some((old_blk_idx, old)) = self.data_blocks_cache.push(*blk_idx, block) {
if old_blk_idx == *blk_idx {
panic!("block already exists, failed to put back block index {} into data blocks cache", blk_idx);
}
self.release(&old);
}
}
}
fn clear_data_blocks_cache(&mut self) {
if self.data_cache_blocks > 0 {
while let Some((_, block)) = self.data_blocks_cache.pop_lru() {
self.release(&block);
}
}
}
fn shutdown(&self) {
self.teardown().expect("local disk cache - failed to tear down cache");
}
}