use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
use bytes::Bytes;
use iroh_blobs::Hash;
pub(crate) const OUTBOARD_CACHE_BYTES: u64 = 256 * 1024 * 1024;
pub(crate) const OUTBOARD_CACHE_ENTRIES: usize = 4096;
#[derive(Debug, Clone)]
pub(crate) struct CachedOutboard {
pub(crate) bytes: Bytes,
pub(crate) origin_ix: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct OutboardCache {
state: Arc<Mutex<State>>,
}
#[derive(Debug)]
struct State {
entries: HashMap<Hash, (CachedOutboard, u64)>,
bytes: u64,
budget: u64,
max_entries: usize,
tick: u64,
}
fn len64(bytes: &Bytes) -> u64 {
u64::try_from(bytes.len()).unwrap_or(u64::MAX)
}
impl State {
fn remove(&mut self, hash: Hash) {
if let Some((entry, _)) = self.entries.remove(&hash) {
self.bytes = self.bytes.saturating_sub(len64(&entry.bytes));
}
}
}
impl OutboardCache {
pub(crate) fn new(budget: u64, max_entries: usize) -> Self {
Self {
state: Arc::new(Mutex::new(State {
entries: HashMap::new(),
bytes: 0,
budget,
max_entries,
tick: 0,
})),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn get(&self, hash: Hash, expected_len: u64) -> Option<CachedOutboard> {
let mut state = self.lock();
let len = len64(&state.entries.get(&hash)?.0.bytes);
if len != expected_len {
state.remove(hash);
return None;
}
state.tick = state.tick.saturating_add(1);
let tick = state.tick;
state.entries.get_mut(&hash).map(|(entry, used)| {
*used = tick;
entry.clone()
})
}
pub(crate) fn insert(&self, hash: Hash, bytes: Bytes, origin_ix: usize) -> bool {
let len = len64(&bytes);
if len == 0 {
return true;
}
let mut state = self.lock();
if len > state.budget || state.max_entries == 0 {
return false;
}
state.remove(hash);
while state.bytes.saturating_add(len) > state.budget
|| state.entries.len() >= state.max_entries
{
let Some(oldest) = state
.entries
.iter()
.min_by_key(|(_, (_, used))| *used)
.map(|(h, _)| *h)
else {
break;
};
state.remove(oldest);
}
state.tick = state.tick.saturating_add(1);
let tick = state.tick;
state.bytes = state.bytes.saturating_add(len);
state
.entries
.insert(hash, (CachedOutboard { bytes, origin_ix }, tick));
true
}
pub(crate) fn evict_if_same(&self, hash: Hash, used: &Bytes) {
let mut state = self.lock();
let same = state.entries.get(&hash).is_some_and(|(entry, _)| {
entry.bytes.as_ptr() == used.as_ptr() && entry.bytes.len() == used.len()
});
if same {
state.remove(hash);
}
}
#[cfg(test)]
pub(crate) fn bytes(&self) -> u64 {
self.lock().bytes
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;