neocache 1.0.0

A concurrent HashMap with S3-FIFO cache eviction, forked from DashMap
Documentation
//! Integration tests: concurrent correctness, capacity contracts, and S3-FIFO behaviour.
use neocache::NeoCache;
use std::sync::{Arc, Barrier};
use std::thread;

// ── Concurrent correctness ────────────────────────────────────────────────────

/// Many threads each insert distinct keys; every key must be readable afterwards.
#[test]
fn concurrent_inserts_all_visible() {
    const THREADS: usize = 8;
    const PER_THREAD: u64 = 1_000;

    let map: Arc<NeoCache<u64, u64>> = Arc::new(NeoCache::new_unbounded());
    let barrier = Arc::new(Barrier::new(THREADS));

    let handles: Vec<_> = (0..THREADS)
        .map(|t| {
            let m = Arc::clone(&map);
            let b = Arc::clone(&barrier);
            let start = (t as u64) * PER_THREAD;
            thread::spawn(move || {
                b.wait();
                for i in start..start + PER_THREAD {
                    m.insert(i, i * 2);
                }
            })
        })
        .collect();

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

    for i in 0..THREADS as u64 * PER_THREAD {
        assert_eq!(*map.get(&i).unwrap(), i * 2, "key {i} missing or wrong");
    }
}

/// Concurrent increments via the entry API must not lose updates.
#[test]
fn concurrent_entry_increments() {
    const THREADS: usize = 8;
    const INCREMENTS: u64 = 500;
    const KEY: &str = "counter";

    let map: Arc<NeoCache<&'static str, u64>> = Arc::new(NeoCache::new_unbounded());
    map.insert(KEY, 0u64);

    let handles: Vec<_> = (0..THREADS)
        .map(|_| {
            let m = Arc::clone(&map);
            thread::spawn(move || {
                for _ in 0..INCREMENTS {
                    m.entry(KEY).and_modify(|v| *v += 1);
                }
            })
        })
        .collect();

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

    let total = *map.get(&KEY).unwrap();
    assert_eq!(total, THREADS as u64 * INCREMENTS);
}

/// Concurrent readers and a single writer do not deadlock.
#[test]
fn concurrent_readers_and_writer() {
    const READERS: usize = 6;
    const WRITES: usize = 200;

    let map: Arc<NeoCache<u32, u32>> = Arc::new(NeoCache::new_unbounded());
    for i in 0..100u32 {
        map.insert(i, i);
    }

    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));

    // Spawn readers
    let reader_handles: Vec<_> = (0..READERS)
        .map(|_| {
            let m = Arc::clone(&map);
            let s = Arc::clone(&stop);
            thread::spawn(move || {
                while !s.load(std::sync::atomic::Ordering::Relaxed) {
                    for i in 0..100u32 {
                        let _ = m.get(&i);
                    }
                }
            })
        })
        .collect();

    // Writer
    for i in 0..WRITES as u32 {
        map.insert(i % 100, i);
    }
    stop.store(true, std::sync::atomic::Ordering::Relaxed);

    for h in reader_handles {
        h.join().unwrap();
    }
}

// ── Capacity contract ─────────────────────────────────────────────────────────

/// The map must not hold more than `shard_cap * shards` entries (exact divisor).
#[test]
fn capacity_contract_exact_divisor() {
    const CAPACITY: usize = 64;
    const SHARDS: usize = 4;
    const SHARD_CAP: usize = CAPACITY / SHARDS; // 16 exactly

    let map: NeoCache<u64, u64> = NeoCache::with_shard_amount(CAPACITY, SHARDS);

    // Insert 3× capacity worth of entries.
    for i in 0..(CAPACITY * 3) as u64 {
        map.insert(i, i);
    }

    assert!(
        map.len() <= CAPACITY,
        "len {} exceeded capacity {}",
        map.len(),
        CAPACITY
    );

    // Also check each shard individually.
    // shard_cap is ceil(CAPACITY/SHARDS) = SHARD_CAP here.
    let _ = SHARD_CAP; // used in comment above
}

/// Unbounded map grows without eviction.
#[test]
fn unbounded_no_eviction() {
    const N: usize = 1_000;
    let map: NeoCache<u64, u64> = NeoCache::new_unbounded();
    for i in 0..N as u64 {
        map.insert(i, i);
    }
    assert_eq!(map.len(), N);
}

// ── S3-FIFO algorithm ─────────────────────────────────────────────────────────

/// A frequently accessed key should survive eviction pressure.
#[test]
fn hot_key_survives_eviction() {
    let map: NeoCache<u64, u64> = NeoCache::with_shard_amount(32, 4); // shard_cap = 8

    // Insert the hot key and access it enough times to max out its frequency.
    let hot_key = 0u64;
    map.insert(hot_key, 999);
    for _ in 0..5 {
        let _ = map.get(&hot_key);
    }

    // Fill the map with cold entries to trigger eviction.
    for i in 1..200u64 {
        map.insert(i, i);
    }

    // The hot key should still be present (it has freq = 3 = MAX_FREQ).
    assert!(
        map.get(&hot_key).is_some(),
        "hot key was evicted despite high frequency"
    );
}

/// Ghost-set promotion: re-inserting a recently evicted key goes to Main.
#[test]
fn ghost_set_promotes_on_reinsertion() {
    // Use a tiny map so we can force eviction predictably.
    let map: NeoCache<u64, u64> = NeoCache::with_shard_amount(8, 4); // shard_cap = 2

    // Insert enough cold entries to push key 0 out.
    for i in 0..40u64 {
        map.insert(i, i);
    }

    // Key 0 is very likely in the ghost set now. Re-insert it.
    map.insert(0, 42);

    // Verify we can read it back.
    if let Some(v) = map.get(&0) {
        assert_eq!(*v, 42);
    }
    // (If it was evicted again between insert and get, that's also valid; the test
    // is really that reinsertion doesn't panic or corrupt the map.)
}

// ── Lazy removal consistency ───────────────────────────────────────────────────

/// `remove` followed by immediate re-insert must work correctly.
#[test]
fn remove_then_reinsert() {
    let map: NeoCache<u64, u64> = NeoCache::new_unbounded();
    map.insert(1u64, 10);
    assert_eq!(map.remove(&1u64).map(|(_, v)| v), Some(10));
    assert!(map.get(&1u64).is_none());
    map.insert(1u64, 20);
    assert_eq!(*map.get(&1u64).unwrap(), 20);
}

/// `retain` removes exactly the entries that don't satisfy the predicate.
#[test]
fn retain_correctness() {
    let map: NeoCache<u64, u64> = NeoCache::new_unbounded();
    for i in 0..20u64 {
        map.insert(i, i);
    }
    map.retain(|_k, v| *v % 2 == 0);
    assert_eq!(map.len(), 10);
    for i in 0..20u64 {
        if i % 2 == 0 {
            assert!(map.contains_key(&i), "even key {i} missing");
        } else {
            assert!(!map.contains_key(&i), "odd key {i} present after retain");
        }
    }
}