haematite 0.3.0

Content-addressed, branchable, actor-native storage engine
Documentation
use std::num::NonZeroUsize;
use std::sync::Arc;

use lru::LruCache as InnerLruCache;

use crate::tree::{Hash, Node};

#[derive(Debug)]
pub struct LruCache {
    nodes: InnerLruCache<Hash, Arc<Node>>,
}

impl LruCache {
    pub fn new(max_entries: usize) -> Result<Self, CacheError> {
        let capacity = NonZeroUsize::new(max_entries).ok_or(CacheError::InvalidCapacity)?;
        Ok(Self {
            nodes: InnerLruCache::new(capacity),
        })
    }

    pub fn get(&mut self, hash: &Hash) -> Option<Arc<Node>> {
        self.nodes.get(hash).cloned()
    }

    pub fn put(&mut self, hash: Hash, node: Arc<Node>) {
        self.nodes.put(hash, node);
    }

    pub fn remove(&mut self, hash: &Hash) -> Option<Arc<Node>> {
        self.nodes.pop(hash)
    }

    pub fn len(&self) -> usize {
        self.nodes.len()
    }

    pub fn is_empty(&self) -> bool {
        self.nodes.is_empty()
    }

    pub fn capacity(&self) -> usize {
        self.nodes.cap().get()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheError {
    InvalidCapacity,
}

impl std::fmt::Display for CacheError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidCapacity => write!(f, "cache capacity must be greater than zero"),
        }
    }
}

impl std::error::Error for CacheError {}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::{CacheError, LruCache};
    use crate::tree::{Hash, LeafNode, Node, NodeError};

    fn node(key: u8, value: &[u8]) -> Result<Arc<Node>, NodeError> {
        LeafNode::new(vec![(vec![key], value.to_vec())])
            .map(Node::Leaf)
            .map(Arc::new)
    }

    #[test]
    fn new_rejects_zero_capacity() {
        let result = LruCache::new(0);
        assert!(matches!(result, Err(CacheError::InvalidCapacity)));
    }

    #[test]
    fn new_uses_configured_capacity() -> Result<(), CacheError> {
        let cache = LruCache::new(3)?;
        assert_eq!(cache.capacity(), 3);
        assert_eq!(cache.len(), 0);
        assert!(cache.is_empty());
        Ok(())
    }

    #[test]
    fn inserting_beyond_capacity_evicts_lru_entry() -> Result<(), Box<dyn std::error::Error>> {
        let mut cache = LruCache::new(2)?;
        let first = node(b'a', b"one")?;
        let second = node(b'b', b"two")?;
        let third = node(b'c', b"three")?;
        let first_hash = first.hash();
        let second_hash = second.hash();
        let third_hash = third.hash();

        cache.put(first_hash, first);
        cache.put(second_hash, second);
        assert_eq!(cache.get(&first_hash), Some(node(b'a', b"one")?));
        cache.put(third_hash, third.clone());

        assert_eq!(cache.len(), 2);
        assert_eq!(cache.get(&second_hash), None);
        assert_eq!(cache.get(&first_hash), Some(node(b'a', b"one")?));
        assert_eq!(cache.get(&third_hash), Some(third));
        Ok(())
    }

    #[test]
    fn remove_evicts_entry_by_hash() -> Result<(), Box<dyn std::error::Error>> {
        let mut cache = LruCache::new(1)?;
        let cached = node(b'a', b"one")?;
        let hash = cached.hash();

        cache.put(hash, cached.clone());
        assert_eq!(cache.remove(&hash), Some(cached));
        assert_eq!(cache.get(&hash), None);
        assert_eq!(cache.remove(&Hash::from_bytes([7; 32])), None);
        Ok(())
    }
}