use std::{
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
};
use crate::layout::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostBuffer {
pub base: usize,
pub len: usize,
}
impl HostBuffer {
pub fn address(&self, range: Range) -> usize {
assert!(
range
.offset
.checked_add(range.size)
.is_some_and(|end| end <= self.len),
"{range:?} is outside the {}-byte Host pool",
self.len
);
self.base + range.offset
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoResult {
pub success: Vec<bool>,
pub retired: bool,
pub object_failures: Vec<bool>,
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct StoreError(pub String);
pub trait Store: Send + Sync {
fn failures_retire(&self) -> bool;
fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError>;
fn batch_get_into(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError>;
fn batch_put_from(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError>;
fn remove_by_prefix(&self, prefix: &str) -> bool;
fn close(&self);
}
impl<S: Store + ?Sized> Store for Arc<S> {
fn failures_retire(&self) -> bool {
(**self).failures_retire()
}
fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
(**self).batch_exists(keys)
}
fn batch_get_into(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
(**self).batch_get_into(keys, host, ranges)
}
fn batch_put_from(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
(**self).batch_put_from(keys, host, ranges)
}
fn remove_by_prefix(&self, prefix: &str) -> bool {
(**self).remove_by_prefix(prefix)
}
fn close(&self) {
(**self).close();
}
}
#[derive(Debug, Default)]
pub struct MemoryStore {
objects: Mutex<HashMap<String, Vec<u8>>>,
}
pub fn process_memory_store() -> Arc<MemoryStore> {
static STORE: OnceLock<Arc<MemoryStore>> = OnceLock::new();
Arc::clone(STORE.get_or_init(Arc::default))
}
impl MemoryStore {
pub fn new() -> Self {
Self::default()
}
pub fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self
.objects
.lock()
.expect("memory store lock")
.keys()
.cloned()
.collect();
keys.sort();
keys
}
pub fn forget(&self, forget: impl Fn(&str) -> bool) {
self.objects
.lock()
.expect("memory store lock")
.retain(|key, _| !forget(key));
}
}
impl Store for MemoryStore {
fn failures_retire(&self) -> bool {
true
}
fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
let objects = self.objects.lock().expect("memory store lock");
Ok(keys.iter().map(|key| objects.contains_key(key)).collect())
}
fn batch_get_into(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
if keys.len() != ranges.len() {
return Err(StoreError("ragged L3 get".to_owned()));
}
let objects = self.objects.lock().expect("memory store lock");
let success: Vec<bool> = keys
.iter()
.zip(ranges)
.map(|(key, &range)| match objects.get(key) {
Some(payload) if payload.len() == range.size => {
#[allow(unsafe_code)]
unsafe {
std::ptr::copy_nonoverlapping(
payload.as_ptr(),
host.address(range) as *mut u8,
range.size,
);
}
true
}
_ => false,
})
.collect();
let object_failures = success.iter().map(|ok| !ok).collect();
Ok(IoResult {
success,
retired: true,
object_failures,
})
}
fn batch_put_from(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
if keys.len() != ranges.len() {
return Err(StoreError("ragged L3 put".to_owned()));
}
let mut objects = self.objects.lock().expect("memory store lock");
for (key, &range) in keys.iter().zip(ranges) {
if objects.contains_key(key) {
continue;
}
let mut payload = vec![0_u8; range.size];
#[allow(unsafe_code)]
unsafe {
std::ptr::copy_nonoverlapping(
host.address(range) as *const u8,
payload.as_mut_ptr(),
range.size,
);
}
objects.insert(key.clone(), payload);
}
Ok(IoResult {
success: vec![true; keys.len()],
retired: true,
object_failures: vec![false; keys.len()],
})
}
fn remove_by_prefix(&self, prefix: &str) -> bool {
self.forget(|key| key.starts_with(prefix));
true
}
fn close(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn objects_round_trip_through_the_host_pool() {
let mut pool = vec![0_u8; 16];
pool[0..4].copy_from_slice(b"abcd");
let host = HostBuffer {
base: pool.as_mut_ptr() as usize,
len: pool.len(),
};
let store = MemoryStore::new();
let keys = vec!["k1".to_owned(), "k2".to_owned()];
let first = Range { offset: 0, size: 4 };
let put = store.batch_put_from(&keys[..1], host, &[first]).unwrap();
assert_eq!(put.success, [true]);
assert_eq!(store.batch_exists(&keys).unwrap(), [true, false]);
let target = Range { offset: 8, size: 4 };
let short = Range {
offset: 12,
size: 3,
};
let got = store
.batch_get_into(&["k1".to_owned(), "k1".to_owned()], host, &[target, short])
.unwrap();
assert_eq!(got.success, [true, false]);
assert_eq!(got.object_failures, [false, true]);
assert!(got.retired);
assert_eq!(&pool[8..12], b"abcd");
assert!(store.remove_by_prefix("k"));
assert!(store.keys().is_empty());
}
}