armdb 0.6.0

sharded bitcask key-value storage optimized for NVMe
Documentation
use quick_cache::Weighter;
use quick_cache::sync::Cache;

use crate::byte_view::ByteView;
use crate::config::CacheConfig;

/// Cache key: a whole value at its exact `DiskLoc` offset. `(file_id, offset)`
/// is globally unique for the life of the data (monotonic file ids), so a key
/// is never reused for different bytes.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ValueKey {
    pub shard_id: u8,
    pub file_id: u32,
    pub offset: u32,
}

#[derive(Clone)]
struct ValueWeighter;

impl Weighter<ValueKey, ByteView> for ValueWeighter {
    fn weight(&self, _key: &ValueKey, value: &ByteView) -> u64 {
        value.len() as u64
    }
}

/// Whole-value cache for large `VarTree`/`VarMap` values (> 8192 bytes) that
/// bypass the 4 KiB [`crate::cache::BlockCache`]. Backed by `quick_cache` with
/// S3-FIFO (scan-resistant) eviction; weighted by value byte length.
pub struct ValueCache {
    inner: Option<Cache<ValueKey, ByteView, ValueWeighter>>,
}

impl ValueCache {
    /// Build a value cache sized by `config`. A `max_size` of `0` disables the
    /// cache: [`insert`](Self::insert) becomes a no-op and [`get`](Self::get)
    /// always returns `None`.
    pub fn new(config: &CacheConfig) -> Self {
        if config.max_size == 0 {
            return Self { inner: None };
        }
        let cache = Cache::with_weighter(config.estimated_items, config.max_size, ValueWeighter);
        Self { inner: Some(cache) }
    }

    /// Return the cached value for `key`, or `None` on a miss (or when the cache
    /// is disabled). A hit clones the stored [`ByteView`] (cheap — refcounted).
    pub fn get(&self, key: &ValueKey) -> Option<ByteView> {
        self.inner.as_ref()?.get(key)
    }

    /// Insert (or overwrite) the value for `key`. A no-op when the cache is
    /// disabled. Eviction is S3-FIFO once the weighted byte budget is exceeded.
    pub fn insert(&self, key: ValueKey, value: ByteView) {
        if let Some(cache) = &self.inner {
            cache.insert(key, value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::CacheConfig;

    fn key(file_id: u32, offset: u32) -> ValueKey {
        ValueKey {
            shard_id: 0,
            file_id,
            offset,
        }
    }

    #[test]
    fn roundtrip_and_disambiguation() {
        let cfg = CacheConfig {
            max_size: 1 << 20,
            estimated_items: 16,
        };
        let cache = ValueCache::new(&cfg);
        cache.insert(key(1, 0), ByteView::new(b"alpha"));
        cache.insert(key(1, 64), ByteView::new(b"beta"));
        cache.insert(key(2, 0), ByteView::new(b"gamma"));
        assert_eq!(cache.get(&key(1, 0)).unwrap().as_bytes(), b"alpha");
        assert_eq!(cache.get(&key(1, 64)).unwrap().as_bytes(), b"beta");
        assert_eq!(cache.get(&key(2, 0)).unwrap().as_bytes(), b"gamma");
        assert!(cache.get(&key(9, 9)).is_none());
    }

    #[test]
    fn disabled_when_max_size_zero() {
        let cfg = CacheConfig {
            max_size: 0,
            estimated_items: 16,
        };
        let cache = ValueCache::new(&cfg);
        cache.insert(key(1, 0), ByteView::new(b"x"));
        assert!(cache.get(&key(1, 0)).is_none());
    }

    #[test]
    fn capacity_pressure_evicts_something() {
        // Budget 1 MiB; insert 64 values of 64 KiB (4 MiB total) -> cannot hold all.
        let cfg = CacheConfig {
            max_size: 1 << 20,
            estimated_items: 64,
        };
        let cache = ValueCache::new(&cfg);
        let blob = ByteView::from_vec(vec![0u8; 64 * 1024]);
        for i in 0..64u32 {
            cache.insert(key(1, i * 64 * 1024), blob.clone());
        }
        // S3-FIFO is scan-resistant, not LRU: do NOT assert which key is gone,
        // only that the bounded cache could not retain all 64.
        let present = (0..64u32)
            .filter(|i| cache.get(&key(1, i * 64 * 1024)).is_some())
            .count();
        assert!(
            present < 64,
            "bounded cache must evict under pressure, got {present}/64"
        );
    }
}