//! Loaded states cached by their heads (hub spec ยง16.3). A state is a pure //! function of its heads, so entries never go stale; the cache is only //! bounded. Shared across threads because `oxy_core::state::State` is //! `Send + Sync`. use oxy_core::hash::ChangeHash; use oxy_core::repo::Repo; use oxy_core::state::{Snapshot, State}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; /// A state and its materialised snapshot. pub struct Loaded { pub heads: Vec, pub state: State, pub snapshot: Snapshot, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] struct Key { repo: [u8; 16], heads: Vec, } struct Entry { loaded: Arc, last_used: u64, } /// A bounded least-recently-used cache of loaded states. pub struct StateCache { capacity: usize, inner: Mutex<(HashMap, u64)>, } impl StateCache { pub fn new(capacity: usize) -> StateCache { StateCache { capacity: capacity.max(1), inner: Mutex::new((HashMap::new(), 0)) } } /// The state with these heads, loaded once per hub process. pub fn load(&self, repo: &Repo, heads: &[ChangeHash]) -> Result, oxy_core::repo::RepoError> { let mut sorted = heads.to_vec(); sorted.sort(); sorted.dedup(); let key = Key { repo: repo.id()?, heads: sorted.clone() }; { let mut guard = self.inner.lock().unwrap(); let (map, tick) = &mut *guard; *tick += 1; if let Some(e) = map.get_mut(&key) { e.last_used = *tick; return Ok(e.loaded.clone()); } } let state = repo.load_state(&sorted)?; let snapshot = state.materialize(); let loaded = Arc::new(Loaded { heads: sorted, state, snapshot }); let mut guard = self.inner.lock().unwrap(); let (map, tick) = &mut *guard; if map.len() >= self.capacity { if let Some(oldest) = map.iter().min_by_key(|(_, e)| e.last_used).map(|(k, _)| k.clone()) { map.remove(&oldest); } } map.insert(key, Entry { loaded: loaded.clone(), last_used: *tick }); Ok(loaded) } pub fn len(&self) -> usize { self.inner.lock().unwrap().0.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } } #[cfg(test)] mod tests { use super::*; #[test] fn caches_by_heads_and_evicts_the_oldest() { let dir = tempfile::tempdir().unwrap(); let repo = Repo::init_bare(dir.path()).unwrap(); let cache = StateCache::new(2); let a = cache.load(&repo, &[]).unwrap(); let b = cache.load(&repo, &[]).unwrap(); assert!(Arc::ptr_eq(&a, &b), "same heads, same entry"); assert_eq!(cache.len(), 1); let h1 = oxy_core::hash::ObjectRef([1; 32]); let h2 = oxy_core::hash::ObjectRef([2; 32]); // Unknown heads fail to load and are not cached. assert!(cache.load(&repo, &[h1]).is_err()); assert_eq!(cache.len(), 1); let _ = (h2, dir); } }