use super::get_page_from_blob;
use crate::{Blob, Error, RwLock};
use commonware_utils::StableBuf;
use futures::{future::Shared, FutureExt};
use std::{
collections::{hash_map::Entry, HashMap},
future::Future,
num::{NonZeroU16, NonZeroUsize},
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
};
use tracing::{debug, error, trace};
type PageFetchFut = Shared<Pin<Box<dyn Future<Output = Result<StableBuf, Arc<Error>>> + Send>>>;
pub struct Pool {
index: HashMap<(u64, u64), usize>,
cache: Vec<CacheEntry>,
clock: usize,
capacity: usize,
page_fetches: HashMap<(u64, u64), PageFetchFut>,
}
struct CacheEntry {
key: (u64, u64),
referenced: AtomicBool,
data: Vec<u8>,
}
#[derive(Clone)]
pub struct PoolRef {
page_size: u64,
next_id: Arc<AtomicU64>,
pool: Arc<RwLock<Pool>>,
}
impl PoolRef {
pub fn new(page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
let page_size = page_size.get() as u64;
Self {
page_size,
next_id: Arc::new(AtomicU64::new(0)),
pool: Arc::new(RwLock::new(Pool::new(capacity.get()))),
}
}
#[inline]
pub const fn page_size(&self) -> u64 {
self.page_size
}
pub async fn next_id(&self) -> u64 {
self.next_id.fetch_add(1, Ordering::Relaxed)
}
pub const fn offset_to_page(&self, offset: u64) -> (u64, u64) {
Pool::offset_to_page(self.page_size, offset)
}
pub(super) async fn read_cached(
&self,
blob_id: u64,
mut buf: &mut [u8],
mut logical_offset: u64,
) -> usize {
let original_len = buf.len();
let buffer_pool = self.pool.read().await;
while !buf.is_empty() {
let count = buffer_pool.read_at(self.page_size, blob_id, buf, logical_offset);
if count == 0 {
break;
}
logical_offset += count as u64;
buf = &mut buf[count..];
}
original_len - buf.len()
}
pub(super) async fn read<B: Blob>(
&self,
blob: &B,
blob_id: u64,
mut buf: &mut [u8],
mut offset: u64,
) -> Result<(), Error> {
while !buf.is_empty() {
{
let buffer_pool = self.pool.read().await;
let count = buffer_pool.read_at(self.page_size, blob_id, buf, offset);
if count != 0 {
offset += count as u64;
buf = &mut buf[count..];
continue;
}
}
let count = self
.read_after_page_fault(blob, blob_id, buf, offset)
.await?;
offset += count as u64;
buf = &mut buf[count..];
}
Ok(())
}
pub(super) async fn read_after_page_fault<B: Blob>(
&self,
blob: &B,
blob_id: u64,
buf: &mut [u8],
offset: u64,
) -> Result<usize, Error> {
assert!(!buf.is_empty());
let (page_num, offset_in_page) = Pool::offset_to_page(self.page_size, offset);
let offset_in_page = offset_in_page as usize;
trace!(page_num, blob_id, "page fault");
let (fetch_future, is_first_fetcher) = {
let mut pool = self.pool.write().await;
let count = pool.read_at(self.page_size, blob_id, buf, offset);
if count != 0 {
return Ok(count);
}
let entry = pool.page_fetches.entry((blob_id, page_num));
match entry {
Entry::Occupied(o) => {
(o.get().clone(), false)
}
Entry::Vacant(v) => {
let blob = blob.clone();
let page_size = self.page_size;
let future = async move {
let page = get_page_from_blob(&blob, page_num, page_size)
.await
.map_err(Arc::new)?;
let len = page.as_ref().len();
if len != page_size as usize {
error!(
page_num,
expected = page_size,
actual = len,
"attempted to fetch partial page from blob"
);
return Err(Arc::new(Error::InvalidChecksum));
}
Ok(page)
};
let shareable = future.boxed().shared();
v.insert(shareable.clone());
(shareable, true)
}
}
};
let fetch_result = fetch_future.await;
if !is_first_fetcher {
let page_buf = fetch_result.map_err(|_| Error::ReadFailed)?;
let bytes_to_copy = std::cmp::min(buf.len(), page_buf.as_ref().len() - offset_in_page);
buf[..bytes_to_copy].copy_from_slice(
&page_buf.as_ref()[offset_in_page..offset_in_page + bytes_to_copy],
);
return Ok(bytes_to_copy);
}
let mut pool = self.pool.write().await;
let _ = pool.page_fetches.remove(&(blob_id, page_num));
let page_buf = match fetch_result {
Ok(page_buf) => page_buf,
Err(err) => {
error!(page_num, ?err, "Page fetch failed");
return Err(Error::ReadFailed);
}
};
pool.cache(self.page_size, blob_id, page_buf.as_ref(), page_num);
let bytes_to_copy = std::cmp::min(buf.len(), page_buf.as_ref().len() - offset_in_page);
buf[..bytes_to_copy]
.copy_from_slice(&page_buf.as_ref()[offset_in_page..offset_in_page + bytes_to_copy]);
Ok(bytes_to_copy)
}
pub async fn cache(&self, blob_id: u64, mut buf: &[u8], offset: u64) -> usize {
let (mut page_num, offset_in_page) = self.offset_to_page(offset);
assert_eq!(offset_in_page, 0);
{
let page_size = self.page_size as usize;
let mut buffer_pool = self.pool.write().await;
while buf.len() >= page_size {
buffer_pool.cache(self.page_size, blob_id, &buf[..page_size], page_num);
buf = &buf[page_size..];
page_num = match page_num.checked_add(1) {
Some(next) => next,
None => break,
};
}
}
buf.len()
}
}
impl Pool {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0);
Self {
index: HashMap::new(),
cache: Vec::new(),
clock: 0,
capacity,
page_fetches: HashMap::new(),
}
}
const fn offset_to_page(page_size: u64, offset: u64) -> (u64, u64) {
(offset / page_size, offset % page_size)
}
fn read_at(&self, page_size: u64, blob_id: u64, buf: &mut [u8], logical_offset: u64) -> usize {
let (page_num, offset_in_page) = Self::offset_to_page(page_size, logical_offset);
let page_index = self.index.get(&(blob_id, page_num));
let Some(&page_index) = page_index else {
return 0;
};
let page = &self.cache[page_index];
assert_eq!(page.key, (blob_id, page_num));
page.referenced.store(true, Ordering::Relaxed);
let page = &page.data;
let logical_page_size = page_size as usize;
let bytes_to_copy = std::cmp::min(buf.len(), logical_page_size - offset_in_page as usize);
buf[..bytes_to_copy].copy_from_slice(
&page[offset_in_page as usize..offset_in_page as usize + bytes_to_copy],
);
bytes_to_copy
}
fn cache(&mut self, page_size: u64, blob_id: u64, page: &[u8], page_num: u64) {
assert_eq!(page.len(), page_size as usize);
let key = (blob_id, page_num);
let index_entry = self.index.entry(key);
if let Entry::Occupied(index_entry) = index_entry {
debug!(blob_id, page_num, "updating duplicate page");
let entry = &mut self.cache[*index_entry.get()];
assert_eq!(entry.key, key);
entry.referenced.store(true, Ordering::Relaxed);
entry.data.copy_from_slice(page);
return;
}
if self.cache.len() < self.capacity {
self.index.insert(key, self.cache.len());
self.cache.push(CacheEntry {
key,
referenced: AtomicBool::new(true),
data: page.into(),
});
return;
}
while self.cache[self.clock].referenced.load(Ordering::Relaxed) {
self.cache[self.clock]
.referenced
.store(false, Ordering::Relaxed);
self.clock = (self.clock + 1) % self.cache.len();
}
let entry = &mut self.cache[self.clock];
entry.referenced.store(true, Ordering::Relaxed);
assert!(self.index.remove(&entry.key).is_some());
self.index.insert(key, self.clock);
entry.key = key;
entry.data.copy_from_slice(page);
self.clock = (self.clock + 1) % self.cache.len();
}
}
#[cfg(test)]
mod tests {
use super::{super::Checksum, *};
use crate::{buffer::pool::CHECKSUM_SIZE, deterministic, Runner as _, Storage as _};
use commonware_cryptography::Crc32;
use commonware_macros::test_traced;
use commonware_utils::{NZUsize, NZU16};
use std::num::NonZeroU16;
const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
const PAGE_SIZE_U64: u64 = PAGE_SIZE.get() as u64;
#[test_traced]
fn test_pool_basic() {
let mut pool: Pool = Pool::new(10);
let mut buf = vec![0; PAGE_SIZE.get() as usize];
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, 0);
assert_eq!(bytes_read, 0);
pool.cache(PAGE_SIZE_U64, 0, &[1; PAGE_SIZE.get() as usize], 0);
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, 0);
assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
assert_eq!(buf, [1; PAGE_SIZE.get() as usize]);
pool.cache(PAGE_SIZE_U64, 0, &[2; PAGE_SIZE.get() as usize], 0);
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, 0);
assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
assert_eq!(buf, [2; PAGE_SIZE.get() as usize]);
for i in 0u64..11 {
pool.cache(PAGE_SIZE_U64, 0, &[i as u8; PAGE_SIZE.get() as usize], i);
}
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, 0);
assert_eq!(bytes_read, 0);
for i in 1u64..11 {
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, i * PAGE_SIZE_U64);
assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
}
let mut buf = vec![0; PAGE_SIZE.get() as usize];
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, PAGE_SIZE_U64 + 2);
assert_eq!(bytes_read, PAGE_SIZE.get() as usize - 2);
assert_eq!(
&buf[..PAGE_SIZE.get() as usize - 2],
[1; PAGE_SIZE.get() as usize - 2]
);
}
#[test_traced]
fn test_pool_read_with_blob() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let physical_page_size = PAGE_SIZE_U64 + CHECKSUM_SIZE;
let (blob, size) = context
.open("test", "blob".as_bytes())
.await
.expect("Failed to open blob");
assert_eq!(size, 0);
for i in 0..11 {
let logical_data = vec![i as u8; PAGE_SIZE.get() as usize];
let crc = Crc32::checksum(&logical_data);
let record = Checksum::new(PAGE_SIZE.get(), crc);
let mut page_data = logical_data;
page_data.extend_from_slice(&record.to_bytes());
blob.write_at(page_data, i * physical_page_size)
.await
.unwrap();
}
let pool_ref = PoolRef::new(PAGE_SIZE, NZUsize!(10));
assert_eq!(pool_ref.next_id().await, 0);
assert_eq!(pool_ref.next_id().await, 1);
for i in 0..11 {
let mut buf = vec![0; PAGE_SIZE.get() as usize];
pool_ref
.read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
.await
.unwrap();
assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
}
for i in 1..11 {
let mut buf = vec![0; PAGE_SIZE.get() as usize];
pool_ref
.read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
.await
.unwrap();
assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
}
blob.sync().await.unwrap();
});
}
#[test_traced]
fn test_pool_cache_max_page() {
let executor = deterministic::Runner::default();
executor.start(|_context| async move {
let pool_ref = PoolRef::new(PAGE_SIZE, NZUsize!(2));
let aligned_max_offset = u64::MAX - (u64::MAX % PAGE_SIZE_U64);
let logical_data = vec![42u8; PAGE_SIZE.get() as usize];
let remaining = pool_ref
.cache(0, logical_data.as_slice(), aligned_max_offset)
.await;
assert_eq!(remaining, 0);
let mut buf = vec![0u8; PAGE_SIZE.get() as usize];
let pool = pool_ref.pool.read().await;
let bytes_read = pool.read_at(PAGE_SIZE_U64, 0, &mut buf, aligned_max_offset);
assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
assert!(buf.iter().all(|b| *b == 42));
});
}
#[test_traced]
fn test_pool_cache_at_high_offset() {
let executor = deterministic::Runner::default();
executor.start(|_context| async move {
const MIN_PAGE_SIZE: u64 = CHECKSUM_SIZE + 1;
let pool_ref = PoolRef::new(NZU16!(MIN_PAGE_SIZE as u16), NZUsize!(2));
let data = vec![1u8; MIN_PAGE_SIZE as usize * 2];
let aligned_max_offset = u64::MAX - (u64::MAX % MIN_PAGE_SIZE);
let high_offset = aligned_max_offset - (MIN_PAGE_SIZE * 2);
let remaining = pool_ref.cache(0, &data, high_offset).await;
assert_eq!(remaining, 0);
let mut buf = vec![0u8; MIN_PAGE_SIZE as usize];
let pool = pool_ref.pool.read().await;
assert_eq!(
pool.read_at(MIN_PAGE_SIZE, 0, &mut buf, high_offset),
MIN_PAGE_SIZE as usize
);
assert!(buf.iter().all(|b| *b == 1));
assert_eq!(
pool.read_at(MIN_PAGE_SIZE, 0, &mut buf, high_offset + MIN_PAGE_SIZE),
MIN_PAGE_SIZE as usize
);
assert!(buf.iter().all(|b| *b == 1));
});
}
}