CacheKit Docs

High-performance cache policies and supporting data structures.

View the Project on GitHub OxidizeLabs/cachekit

Read-Only Cache Traits

This guide explains how to use read-only traits for safe, side-effect-free cache inspection.

Overview

CacheKit provides a comprehensive set of read-only traits that allow you to inspect cache state without triggering evictions, updating access patterns, or requiring mutable access. This is useful for:

Trait Hierarchy

ReadOnlyCache<K, V>              ← Base read-only trait
    ├── CoreCache<K, V>          ← Adds insert/get (extends ReadOnlyCache)
    │   ├── FifoCacheTrait       ← FIFO-specific operations
    │   └── MutableCache         ← Adds remove operation
    │       ├── LruCacheTrait
    │       ├── LfuCacheTrait
    │       └── LrukCacheTrait
    │
    └── Policy-Specific Read-Only Traits
        ├── ReadOnlyFifoCache    ← FIFO inspection
        ├── ReadOnlyLruCache     ← LRU inspection
        ├── ReadOnlyLfuCache     ← LFU inspection
        └── ReadOnlyLruKCache    ← LRU-K inspection

Base Read-Only Trait

The ReadOnlyCache<K, V> trait provides fundamental inspection operations:

use cachekit::traits::ReadOnlyCache;
use cachekit::policy::lru_k::LrukCache;

fn print_cache_stats<C: ReadOnlyCache<u64, String>>(cache: &C) {
    println!("Cache size: {}/{}", cache.len(), cache.capacity());
    println!("Is empty: {}", cache.is_empty());
    println!("Has key 42: {}", cache.contains(&42));
}

let mut cache = LrukCache::new(100);
cache.insert(42, "answer".to_string());

// Works with immutable reference - no side effects!
print_cache_stats(&cache);

Methods

Method Description Side Effects
contains(&K) -> bool Check if key exists None
len() -> usize Get number of entries None
is_empty() -> bool Check if cache is empty None
capacity() -> usize Get maximum capacity None

Policy-Specific Read-Only Traits

Each cache policy has a corresponding read-only trait for inspecting policy-specific metadata.

FIFO: ReadOnlyFifoCache

Inspect insertion order without modifying the queue:

use cachekit::traits::{ReadOnlyFifoCache, CoreCache};
use cachekit::policy::fifo::FifoCache;

fn analyze_fifo_state<C: ReadOnlyFifoCache<u64, String>>(cache: &C) {
    // Peek at oldest entry without removing it
    if let Some((key, value)) = cache.peek_oldest() {
        println!("Oldest: key={}, value={}", key, value);

        // Get its position in the queue
        if let Some(rank) = cache.age_rank(key) {
            println!("Age rank: {} (0 = oldest)", rank);
        }
    }
}

let mut cache = FifoCache::new(10);
cache.insert(1, "first".to_string());
cache.insert(2, "second".to_string());

analyze_fifo_state(&cache);  // No side effects!

Methods:

LRU: ReadOnlyLruCache

Inspect recency order without affecting LRU list:

use std::sync::Arc;
use cachekit::traits::{ReadOnlyLruCache, CoreCache};
use cachekit::policy::lru::LruCore;

fn analyze_lru_state<C: ReadOnlyLruCache<u64, String>>(cache: &C) {
    // Peek at least recently used entry
    if let Some((key, value)) = cache.peek_lru() {
        println!("LRU: key={}, value={}", key, value);

        // Get recency rank (0 = most recent)
        if let Some(rank) = cache.recency_rank(key) {
            println!("Recency rank: {}", rank);
        }
    }
}

let mut cache: LruCore<u64, &str> = LruCore::new(10);
cache.insert(1, Arc::new("first"));
cache.insert(2, Arc::new("second"));
cache.get(&1);  // Make key 1 MRU

analyze_lru_state(&cache);  // Doesn't change LRU order!

Methods:

LFU: ReadOnlyLfuCache

Inspect frequency information without updating counters:

use std::sync::Arc;
use cachekit::traits::{ReadOnlyLfuCache, CoreCache};
use cachekit::policy::lfu::LfuCache;

fn analyze_lfu_state<C: ReadOnlyLfuCache<u64, String>>(cache: &C) {
    // Peek at least frequently used entry
    if let Some((key, value)) = cache.peek_lfu() {
        println!("LFU: key={}, value={}", key, value);

        // Get access frequency
        if let Some(freq) = cache.frequency(key) {
            println!("Frequency: {} accesses", freq);
        }
    }
}

let mut cache: LfuCache<u64, &str> = LfuCache::new(10);
cache.insert(1, Arc::new("value"));
cache.get(&1);  // Frequency = 2 (insert + get)
cache.get(&1);  // Frequency = 3

analyze_lfu_state(&cache);  // Doesn't increment frequency!

Methods:

LRU-K: ReadOnlyLruKCache

Inspect K-distance and access history without recording accesses:

use cachekit::traits::{ReadOnlyLruKCache, CoreCache};
use cachekit::policy::lru_k::LrukCache;

fn analyze_lru_k_state<C: ReadOnlyLruKCache<u64, String>>(cache: &C) {
    println!("K value: {}", cache.k_value());

    if let Some((key, value)) = cache.peek_lru_k() {
        println!("LRU-K: key={}, value={}", key, value);

        // Get access statistics
        if let Some(count) = cache.access_count(key) {
            println!("Access count: {}", count);
        }

        if let Some(dist) = cache.k_distance(key) {
            println!("K-distance: {}", dist);
        }

        if let Some(history) = cache.access_history(key) {
            println!("Access history (recent first): {:?}", history);
        }

        if let Some(rank) = cache.k_distance_rank(key) {
            println!("K-distance rank: {} (0 = evict first)", rank);
        }
    }
}

let mut cache = LrukCache::with_k(10, 2);
cache.insert(1, "value".to_string());
cache.get(&1);  // Second access

analyze_lru_k_state(&cache);  // No access recorded!

Methods:

Use Cases

1. Monitoring & Observability

use cachekit::traits::{ReadOnlyCache, ReadOnlyLruCache};

fn cache_health_check<C>(cache: &C) -> CacheHealth
where
    C: ReadOnlyCache<u64, Vec<u8>> + ReadOnlyLruCache<u64, Vec<u8>>,
{
    let utilization = cache.len() as f64 / cache.capacity() as f64;
    let has_lru_victim = cache.peek_lru().is_some();

    CacheHealth {
        size: cache.len(),
        utilization: (utilization * 100.0) as u8,
        can_evict: has_lru_victim,
    }
}

struct CacheHealth {
    size: usize,
    utilization: u8,  // 0-100%
    can_evict: bool,
}

2. Testing Without Side Effects

#[cfg(test)]
mod tests {
    use cachekit::traits::{ReadOnlyLruCache, CoreCache};
    use cachekit::policy::lru::LruCore;
    use std::sync::Arc;

    #[test]
    fn test_lru_order_preserved() {
        let mut cache: LruCore<u64, &str> = LruCore::new(3);
        cache.insert(1, Arc::new("a"));
        cache.insert(2, Arc::new("b"));
        cache.insert(3, Arc::new("c"));

        // Inspect without affecting order
        assert_eq!(cache.peek_lru().map(|(k, _)| *k), Some(1));
        assert_eq!(cache.recency_rank(&1), Some(2));  // Oldest
        assert_eq!(cache.recency_rank(&3), Some(0));  // Newest

        // Verify peek didn't change order
        assert_eq!(cache.peek_lru().map(|(k, _)| *k), Some(1));
    }
}

3. Concurrent Readers

use std::sync::Arc;
use parking_lot::RwLock;
use cachekit::traits::{ReadOnlyCache, ReadOnlyLruCache};
use cachekit::policy::lru::LruCore;

// Shared cache with many readers
let cache = Arc::new(RwLock::new(LruCore::<u64, Arc<String>>::new(1000)));

// Spawn multiple reader threads
let handles: Vec<_> = (0..10).map(|i| {
    let cache = cache.clone();
    std::thread::spawn(move || {
        // Only need read lock for inspection
        let guard = cache.read();

        println!("Thread {}: size={}, has_42={}",
            i, guard.len(), guard.contains(&42));

        if let Some((key, _)) = guard.peek_lru() {
            println!("Thread {}: LRU key={}", i, key);
        }
    })
}).collect();

for handle in handles {
    handle.join().unwrap();
}

4. Clear API Intent

use cachekit::traits::{ReadOnlyCache, CoreCache};

// Function signature clearly shows it won't modify cache
fn compute_hit_rate<C: ReadOnlyCache<u64, Vec<u8>>>(
    cache: &C,
    hits: u64,
    total: u64,
) -> f64 {
    // Can inspect but not modify
    let size = cache.len();
    println!("Cache size: {}", size);

    hits as f64 / total as f64
}

// This function CAN modify cache
fn warm_cache<C: CoreCache<u64, Vec<u8>>>(
    cache: &mut C,
    data: &[(u64, Vec<u8>)],
) {
    for (k, v) in data {
        cache.insert(*k, v.clone());
    }
}

Comparison: Read-Only vs Mutable Operations

Operation Read-Only Trait Mutable Operation Side Effects
Check existence contains(&K) get(&mut K) None vs Updates access
View LRU peek_lru() get(&mut K) None vs Moves to MRU
View frequency frequency(&K) get(&mut K) None vs Increments count
View oldest peek_oldest() pop_oldest() None vs Removes entry

Best Practices

  1. Use &self for Inspection: Read-only traits work with immutable references
    fn inspect<C: ReadOnlyCache<K, V>>(cache: &C) { }  // ✓ Good
    fn inspect<C: CoreCache<K, V>>(cache: &mut C) { }  // ✗ Overkill
    
  2. Combine Traits for Rich Inspection: Use multiple trait bounds
    fn analyze<C>(cache: &C)
    where
        C: ReadOnlyCache<u64, String> + ReadOnlyLruCache<u64, String>,
    {
        // Can use both base and policy-specific inspection
    }
    
  3. Prefer Read-Only for Metrics: Collecting metrics shouldn’t affect behavior
    struct MetricsCollector;
    
    impl MetricsCollector {
        fn collect<C: ReadOnlyCache<K, V>>(&self, cache: &C) -> Metrics {
            Metrics {
                size: cache.len(),
                capacity: cache.capacity(),
                utilization: cache.len() as f64 / cache.capacity() as f64,
            }
        }
    }
    
  4. Document Side Effects: Make it clear when functions modify cache state
    /// Inspects cache without side effects.
    fn peek<C: ReadOnlyCache<K, V>>(cache: &C) { }
    
    /// Updates LRU order as a side effect.
    fn access<C: CoreCache<K, V>>(cache: &mut C, key: &K) {
        cache.get(key);
    }
    

Performance Considerations

See Also