Skip to main content

Crate priority_lfu

Crate priority_lfu 

Source
Expand description

Β§Priority LFU Cache

A high-performance, concurrent, in-memory cache with weight-stratified clock eviction policy and policy-based prioritization.

Β§Features

  • πŸš€ High Performance: Sub-microsecond reads with minimal overhead
  • πŸ“ Size-Bounded: Memory limits in bytes, not item count
  • βš–οΈ Policy-Based Eviction: Prioritize important items with 4 eviction tiers
  • 🎯 Weight-Stratified Clock: Predictable priority-based eviction with frequency tracking
  • πŸ”’ Thread-Safe: Fine-grained sharding for low contention
  • πŸ”„ Async-Friendly: Safe to use in async/await contexts
  • 🎨 Heterogeneous Storage: Store different key/value types without enums

Β§Quick Start

Add to your Cargo.toml:

[dependencies]
priority-lfu = "0.1"

Basic usage:

use priority_lfu::{Cache, CacheKey, CachePolicy, DeepSizeOf};

// Define your key type
#[derive(Hash, Eq, PartialEq, Clone)]
struct UserId(u64);

// Define your value type
#[derive(Clone, Debug, PartialEq, DeepSizeOf)]
struct UserData {
    name: String,
    score: i32,
}

// Implement required traits
impl CacheKey for UserId {
    type Value = UserData;

    fn policy(&self) -> CachePolicy {
        // Different eviction priorities:
        // Critical - last to be evicted (metadata, schemas)
        // Standard - normal eviction (default)
        // Volatile - first to be evicted (temp data)
        
        // Example: VIP users get higher priority based on ID
        if self.0 < 1000 { 
            CachePolicy::Critical 
        } else { 
            CachePolicy::Standard 
        }
    }
}

fn main() {
    // Create cache with 1GB capacity
    let cache = Cache::new(1024 * 1024 * 1024);

    // Insert values
    cache.insert(UserId(1), UserData {
        name: "Alice".to_string(),
        score: 1500,
    });

    // Retrieve values
    if let Some(user) = cache.get_clone(&UserId(1)) {
        println!("User: {}, Score: {}", user.name, user.score);
    }
}

Β§Async Usage

The cache is safe to use in async contexts:

β“˜
async fn process_user(cache: Arc<Cache>, user_id: UserId) {
    // βœ… Use get_clone() - releases lock immediately
    if let Some(user) = cache.get_clone(&user_id) {
        // Safe to hold Arc across await points
        expensive_async_operation(&user).await;
    }
}

⚠️ Warning: Don’t hold Guard across .await points:

β“˜
// ❌ BAD: Guard holds a lock
let guard = cache.get(&key).unwrap();
some_async_fn().await; // Will fail to compile in Send context

// βœ… GOOD: Extract data, drop guard, then await
let data = {
    let guard = cache.get(&key).unwrap();
    guard.clone()
}; // Guard dropped here
some_async_fn().await;

Β§Advanced Configuration

Use CacheBuilder for custom settings:

β“˜
use priority_lfu::CacheBuilder;

let cache = CacheBuilder::new(1024 * 1024 * 512) // 512 MB
    .shards(128)                // More shards = less contention
    .build();

Β§Metrics

The cache provides comprehensive performance metrics for monitoring and debugging.

Note: Metrics tracking is behind the metrics feature flag and not enabled by default to minimize overhead. To use metrics, add the feature to your Cargo.toml:

[dependencies]
priority-lfu = { version = "0.1", features = ["metrics"] }

Example usage:

β“˜
use priority_lfu::Cache;

let cache = Cache::new(1024 * 1024);

// Perform some operations
cache.insert(key1, value1);
cache.insert(key2, value2);
cache.get_clone(&key1);  // hit
cache.get_clone(&key3);  // miss

// Get metrics snapshot
let metrics = cache.metrics();

// Access counters
println!("Cache hits: {}", metrics.hits);
println!("Cache misses: {}", metrics.misses);
println!("Inserts: {}", metrics.inserts);
println!("Updates: {}", metrics.updates);
println!("Evictions: {}", metrics.evictions);
println!("Removals: {}", metrics.removals);

// Computed metrics
println!("Hit rate: {:.2}%", metrics.hit_rate() * 100.0);
println!("Utilization: {:.2}%", metrics.utilization() * 100.0);
println!("Total accesses: {}", metrics.total_accesses());
println!("Total writes: {}", metrics.total_writes());

// Size and capacity
println!("Current size: {} bytes", metrics.current_size_bytes);
println!("Capacity: {} bytes", metrics.capacity_bytes);
println!("Entry count: {}", metrics.entry_count);

Β§Available Metrics

MetricTypeDescription
hitsu64Number of successful cache lookups
missesu64Number of failed cache lookups (key not found)
insertsu64Number of new entries inserted
updatesu64Number of existing entries updated (key already existed)
evictionsu64Number of entries evicted due to capacity constraints
removalsu64Number of entries explicitly removed via remove()
current_size_bytesusizeCurrent total size in bytes
capacity_bytesusizeMaximum capacity in bytes
entry_countusizeCurrent number of entries

Β§Computed Metrics

The CacheMetrics struct provides helper methods for computed metrics:

  • hit_rate() - Returns cache hit rate as a ratio (0.0 to 1.0)
  • utilization() - Returns memory utilization as a ratio (0.0 to 1.0)
  • total_accesses() - Returns total cache accesses (hits + misses)
  • total_writes() - Returns total write operations (inserts + updates)

Β§Integration Example

Example of integrating with a monitoring system:

β“˜
use std::time::Duration;
use std::sync::Arc;

async fn monitor_cache(cache: Arc<Cache>) {
    loop {
        tokio::time::sleep(Duration::from_secs(60)).await;
        
        let metrics = cache.metrics();
        
        // Send to your monitoring system (Prometheus, etc.)
        if metrics.hit_rate() < 0.5 {
            eprintln!("Warning: Cache hit rate is low: {:.2}%", 
                     metrics.hit_rate() * 100.0);
        }
        
        if metrics.utilization() > 0.9 {
            eprintln!("Warning: Cache utilization is high: {:.2}%",
                     metrics.utilization() * 100.0);
        }
    }
}

Β§Performance Impact

Metrics tracking adds overhead through atomic operations on every cache access:

  • 6+ atomic operations per insert (insert/update/eviction tracking)
  • 2 atomic operations per get (hit/miss tracking)
  • Additional memory for storing 6 AtomicU64 counters

If you don’t need metrics, omit the metrics feature for maximum performance. The default build (without metrics) eliminates all tracking overhead.

Β§How It Works

Β§Weight-Stratified Clock Algorithm

The cache uses a weight-stratified clock eviction policy that maintains four priority buckets per shard:

  1. Critical Bucket (CachePolicy::Critical): Metadata, schemas, indexes - last to evict
  2. Standard Bucket (CachePolicy::Standard): Normal cacheable data - default priority
  3. Volatile Bucket (CachePolicy::Volatile): Temp data, intermediate results - first to evict
Β§Eviction Process

When space is needed, the cache uses a clock sweep starting from the lowest priority bucket:

  1. Start with Volatile bucket, advance clock hand
  2. For each entry encountered:
    • If clock_bit is set β†’ clear it and advance
    • If clock_bit is clear and frequency = 0 β†’ evict
    • If clock_bit is clear and frequency > 0 β†’ decrement frequency and advance
  3. If bucket is empty, move to next higher priority bucket (Volatile β†’ Standard β†’ Critical)

Each access sets the clock_bit and increments frequency (saturating at 255).

Β§Policy-Based Eviction

This design provides predictable priority-based eviction:

β“˜
// Eviction order: Volatile β†’ Standard β†’ Critical
// Within each bucket: Clock sweep with LFU tie-breaking

CachePolicy::Volatile   // Evicted first
CachePolicy::Standard   // Normal eviction
CachePolicy::Critical   // Evicted last

Benefits:

  • Predictable: Lower priority items always evicted before higher priority
  • Fast: Only scans entries in the lowest-priority non-empty bucket
  • Simple: No complex hot/cold promotion logic

Β§Architecture

β“˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Cache (Send + Sync)         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Global State (atomics)          β”‚  β”‚
β”‚  β”‚ - current_size: AtomicUsize     β”‚  β”‚
β”‚  β”‚ - entry_count: AtomicUsize      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚Shard0β”‚ β”‚Shard1β”‚ ...  β”‚ShardNβ”‚      β”‚
β”‚  β”‚RwLockβ”‚ β”‚RwLockβ”‚      β”‚RwLockβ”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each shard contains:

  • Policy Buckets: Four priority buckets (Critical, Standard, Volatile)
  • Clock Hands: One per bucket for efficient clock sweep
  • Entry Map: HashMap with pre-computed hashes for O(1) lookup

Β§Performance

  • Reads: Sub-microsecond with atomic clock_bit and frequency updates
  • Writes: Fast eviction by scanning only lowest priority bucket
  • Concurrency: Default 64 shards for low contention
  • Memory: ~96 bytes overhead per entry (no ghost list needed)

Β§Thread Safety

The cache is Send + Sync and can be shared via Arc:

β“˜
use std::sync::Arc;
use priority_lfu::Cache;

let cache = Arc::new(Cache::new(1024 * 1024));

let handles: Vec<_> = (0..4)
    .map(|_| {
        let cache = cache.clone();
        std::thread::spawn(move || {
            // Safe concurrent access
            cache.insert(key, value);
        })
    })
    .collect();

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

Β§Testing

Run tests:

cargo test

Run benchmarks:

cargo bench

Β§License

This project is licensed under the MIT License.

Β§Contributing

For maintainers releasing new versions, see RELEASE.md for the release process.

Β§Design

For detailed design documentation, see design/spec.md.

Β§References

MacrosΒ§

known_deep_size
A macro to generate an impl for types with known inner allocation sizes.

StructsΒ§

Cache
Thread-safe cache with weight-stratified clock eviction.
CacheBuilder
Builder for configuring a Cache.
Context
The context of which references have already been seen. This should only be used in the implementation of the deep_size_of_children function, and (eventually, when this reaches 0.2) will not be able to be constructed, and only obtained from inside the function.
Guard
RAII guard for borrowed values. Holds a read lock on the shard.

EnumsΒ§

CachePolicy
Cache eviction policy determining retention priority.

TraitsΒ§

CacheKey
Marker trait for cache keys. Associates a key type with its value type.
DeepSizeOf
A trait for measuring the size of an object and its children

Derive MacrosΒ§

DeepSizeOf