dualcache-ff 0.5.0

A wait-free, high-performance concurrent cache optimized for extreme read-to-write ratios.
Documentation
use dualcache_ff::{Config, DualCacheFF};
use std::sync::Arc;

#[cfg(feature = "loom")]
use loom::thread;
#[cfg(not(feature = "loom"))]
use std::thread;

#[cfg(feature = "loom")]
use loom::sync::atomic::{AtomicUsize, Ordering};
#[cfg(not(feature = "loom"))]
use std::sync::atomic::{AtomicUsize, Ordering};

#[cfg(feature = "loom")]
use loom::sync::Mutex;
#[cfg(not(feature = "loom"))]
use std::sync::Mutex;

use std::collections::HashMap;

use crate::common::run_with_timeout;

#[cfg(feature = "loom")]
macro_rules! test_runner {
    ($name:ident, $body:expr) => {
        #[test]
        fn $name() {
            let mut builder = loom::model::Builder::new();
            builder.preemption_bound = Some(2);
            builder.check(|| {
                $body
            });
        }
    };
}

#[cfg(not(feature = "loom"))]
macro_rules! test_runner {
    ($name:ident, $body:expr) => {
        #[test]
        fn $name() {
            run_with_timeout(std::time::Duration::from_secs(10), || {
                $body
            });
        }
    };
}

test_runner!(test_concurrent_ops, {
    let config = Config::new_expert(128, 64, 64, 200, 128);
    let cache = DualCacheFF::new(config);
    
    let ops = Arc::new(AtomicUsize::new(0));
    let shadow = Arc::new(Mutex::new(HashMap::new()));

    let mut handles = vec![];
    
    for i in 0..2 {
        let c = cache.clone();
        let ops_clone = ops.clone();
        let shadow_clone = shadow.clone();
        let handle = thread::spawn(move || {
            let offset = i * 100;
            c.insert(offset, offset);
            shadow_clone.lock().unwrap().insert(offset, offset);
            
            c.insert(offset + 1, offset + 1);
            shadow_clone.lock().unwrap().insert(offset + 1, offset + 1);
            
            let _ = c.get(&offset);
            
            c.remove(&(offset + 1));
            shadow_clone.lock().unwrap().remove(&(offset + 1));
            
            ops_clone.fetch_add(4, Ordering::Relaxed);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    assert_eq!(ops.load(Ordering::Relaxed), 8);

    #[cfg(not(feature = "loom"))]
    {
        // Wait for Daemon to converge in real execution
        cache.sync();
        let shadow_map = shadow.lock().unwrap();
        let mut found = 0;
        for k in shadow_map.keys() {
            if let Some(actual_v) = cache.get(k) {
                let expected_v = shadow_map.get(k).unwrap();
                assert_eq!(*expected_v, actual_v);
                found += 1;
            }
        }
        assert!(found > 0, "Vacuous assertion: zero keys found");
    }
});

#[cfg(not(feature = "loom"))]
#[test]
fn test_ttl_mechanic() {
    run_with_timeout(std::time::Duration::from_secs(10), || {
        use std::time::Duration;
        
        let config = Config::new_expert(128, 64, 64, 1, 128);
        let cache = DualCacheFF::new(config);
        
        // Insert 64 times to trigger L1 Lossy Filter (>=10 hits) and sharded batch flush (64 items)
        for _ in 0..64 {
            cache.insert(1, 100);
        }
        
        // Wait for insertion
        cache.sync();
        assert_eq!(cache.get(&1), Some(100));
        
        // Poll for TTL expiration instead of a hard 2-second sleep
        let mut expired = false;
        for _ in 0..25 {
            if cache.get(&1).is_none() {
                expired = true;
                break;
            }
            thread::sleep(Duration::from_millis(100));
        }
        
        assert!(expired, "TTL Mechanic failed: item did not expire within 2.5 seconds");
    });
}

test_runner!(test_concurrent_insert_t1, {
    let config = Config::new_expert(128, 64, 64, 200, 128);
    let cache = DualCacheFF::new(config);
    
    let ops = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];
    
    for i in 0..2 {
        let c = cache.clone();
        let ops_clone = ops.clone();
        let handle = thread::spawn(move || {
            let offset = i * 100;
            let session = c.begin_cold_start_session();
            session.warmup(offset, offset);
            session.warmup(offset + 1, offset + 1);
            let _ = c.get(&offset);
            ops_clone.fetch_add(2, Ordering::Relaxed);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    assert_eq!(ops.load(Ordering::Relaxed), 4);

    #[cfg(not(feature = "loom"))]
    {
        cache.sync();
        assert_eq!(cache.get(&0), Some(0));
        assert_eq!(cache.get(&1), Some(1));
        assert_eq!(cache.get(&100), Some(100));
        assert_eq!(cache.get(&101), Some(101));
    }
});