imc 0.1.4

Trait-based, deduplicating, in-memory caching library for Rust
use std::any::TypeId;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::{OnceLock, RwLock};
use std::time::Duration;

use crate::entry::Entry;
use crate::hasher::tick;
use crate::traits::{CacheStrategy, ImcCacheable};

// ---------------------------------------------------------------------------
// Per-type cache
// ---------------------------------------------------------------------------

pub(crate) struct PerTypeCache {
    strategy: CacheStrategy,
    max_size: usize,
    data: HashMap<u64, Entry>,
    index: HashMap<u64, u64>,
}

impl PerTypeCache {
    pub(crate) fn from_trait<T: ImcCacheable>() -> Self {
        #[cfg(feature = "invalidation-redis")]
        crate::invalidation::register::<T>();
        Self {
            strategy: T::cache_strategy(),
            max_size: T::cache_max_size(),
            data: HashMap::new(),
            index: HashMap::new(),
        }
    }

    pub(crate) fn get<V: Clone + Send + Sync + 'static>(&self, args_hash: u64) -> Option<V> {
        let id_hash = self.index.get(&args_hash)?;
        let entry = self.data.get(id_hash)?;

        if let Some(ttl) = entry.ttl {
            if entry.created_at.elapsed() > ttl {
                crate::metrics::record_miss();
                return None;
            }
        }

        let v = entry.value.downcast_ref::<V>()?.clone();
        entry.access_count.fetch_add(1, Ordering::Relaxed);
        entry.last_accessed.store(tick(), Ordering::Relaxed);
        crate::metrics::record_hit();
        Some(v)
    }

    pub(crate) fn set<V: Send + Sync + 'static>(
        &mut self,
        args_hash: u64,
        id_hash: u64,
        value: V,
        ttl: Option<Duration>,
    ) {
        let expired = self.data.get(&id_hash).map_or(false, |e| {
            e.ttl.map_or(false, |ttl| e.created_at.elapsed() > ttl)
        });
        if expired {
            self.data.remove(&id_hash);
        }

        if self.data.contains_key(&id_hash) {
            self.index.insert(args_hash, id_hash);
            crate::metrics::record_set();
            return;
        }

        if self.data.len() >= self.max_size
            && crate::worker::WORKER_TX.lock().unwrap_or_else(|e| e.into_inner()).is_none()
        {
            if let Some(evict_id) = self.evict_candidate() {
                self.data.remove(&evict_id);
                crate::metrics::record_eviction();
                crate::log_event!(DEBUG, crate::log::CACHE, crate::log::EVICT, id_hash = evict_id);
            }
        }

        self.data.insert(id_hash, Entry::new(value, ttl));
        self.index.insert(args_hash, id_hash);
        crate::metrics::record_set();
    }

    fn evict_candidate(&self) -> Option<u64> {
        if self.data.is_empty() {
            return None;
        }

        match self.strategy {
            CacheStrategy::Lru => self
                .data
                .iter()
                .min_by_key(|(_, e)| e.last_accessed.load(Ordering::Relaxed))
                .map(|(k, _)| *k),
            CacheStrategy::Mru => self
                .data
                .iter()
                .max_by_key(|(_, e)| e.last_accessed.load(Ordering::Relaxed))
                .map(|(k, _)| *k),
            CacheStrategy::Lfu => self
                .data
                .iter()
                .min_by_key(|(_, e)| e.access_count.load(Ordering::Relaxed))
                .map(|(k, _)| *k),
            CacheStrategy::Mfu => self
                .data
                .iter()
                .max_by_key(|(_, e)| e.access_count.load(Ordering::Relaxed))
                .map(|(k, _)| *k),
            CacheStrategy::Fifo => self
                .data
                .iter()
                .min_by_key(|(_, e)| e.inserted_at)
                .map(|(k, _)| *k),
        }
    }

    pub(crate) fn remove_data(&mut self, id_hash: u64) {
        self.data.remove(&id_hash);
        crate::log_event!(DEBUG, crate::log::CACHE, crate::log::REMOVE, id_hash = id_hash);
    }

    #[cfg(feature = "critical")]
    pub(crate) fn remove_by_args_hash(&mut self, args_hash: u64) {
        if let Some(id_hash) = self.index.remove(&args_hash) {
            let still_referenced = self.index.values().any(|v| *v == id_hash);
            if !still_referenced {
                self.data.remove(&id_hash);
            }
            crate::log_event!(DEBUG, crate::log::CACHE, crate::log::REMOVE,
                args_hash = args_hash, id_hash = id_hash);
        }
    }

    pub(crate) fn clear(&mut self) {
        let count = self.data.len();
        self.data.clear();
        self.index.clear();
        crate::metrics::record_expired(count as u64);
        crate::log_event!(INFO, crate::log::CACHE, crate::log::CLEAR, evicted = count);
    }

    pub(crate) fn len(&self) -> usize {
        self.data.len()
    }

    pub(crate) fn remove_expired(&mut self) {
        let before = self.data.len();
        self.data.retain(|_, e| {
            e.ttl.map_or(true, |ttl| e.created_at.elapsed() <= ttl)
        });
        let removed = before - self.data.len();
        self.index.retain(|_, id_hash| self.data.contains_key(id_hash));
        if removed > 0 {
            crate::metrics::record_expired(removed as u64);
            crate::log_event!(DEBUG, crate::log::CACHE, crate::log::EXPIRY, count = removed);
        }
    }

    pub(crate) fn evict_to_max_size(&mut self) {
        let mut evicted = 0u64;
        while self.data.len() > self.max_size {
            if let Some(evict_id) = self.evict_candidate() {
                self.data.remove(&evict_id);
                evicted += 1;
            }
        }
        self.index.retain(|_, id_hash| self.data.contains_key(id_hash));
        if evicted > 0 {
            crate::metrics::record_eviction();
            crate::log_event!(DEBUG, crate::log::CACHE, crate::log::EVICT, count = evicted);
        }
    }
}

// ---------------------------------------------------------------------------
// Global cache registry
// ---------------------------------------------------------------------------

pub(crate) struct GlobalCache {
    pub(crate) stores: RwLock<HashMap<TypeId, PerTypeCache>>,
}

impl GlobalCache {
    fn new() -> Self {
        Self {
            stores: RwLock::new(HashMap::new()),
        }
    }
}

pub(crate) fn global() -> &'static GlobalCache {
    static G: OnceLock<GlobalCache> = OnceLock::new();
    G.get_or_init(GlobalCache::new)
}

pub(crate) fn global_init() {
    global();
}