uri-register 0.3.0

A high-performance PostgreSQL-backed URI dictionary service for assigning unique integer IDs to URIs
Documentation
// Copyright TELICENT LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Cache implementations for URI-to-ID mapping
//!
//! This module provides different caching strategies for the URI register:
//! - **Moka** (W-TinyLFU): Default. Better hit rates for most workloads
//! - **LRU**: Simple least-recently-used eviction policy
//!
//! W-TinyLFU (Window Tiny Least Frequently Used) combines recency and frequency
//! tracking to provide better cache admission policies compared to plain LRU.

use lru::LruCache;
use moka::sync::Cache as MokaCache;
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};

/// Cache strategy for URI-to-ID mapping
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CacheStrategy {
    /// LRU (Least Recently Used) cache
    /// Simple eviction based on recency of access
    Lru,

    /// Moka cache with W-TinyLFU admission policy
    /// Better hit rates by considering both recency and frequency
    /// This is the default and recommended strategy
    #[default]
    Moka,
}

/// Cache performance statistics for observability
#[derive(Debug, Clone)]
pub struct CacheStats {
    /// Number of cache hits
    pub hits: u64,
    /// Number of cache misses
    pub misses: u64,
    /// Current number of entries in the cache
    pub entry_count: u64,
    /// Maximum capacity of the cache
    pub capacity: u64,
}

impl CacheStats {
    /// Calculate hit rate as a percentage (0.0 to 100.0)
    pub fn hit_rate(&self) -> f64 {
        let total = self.hits + self.misses;
        if total == 0 {
            0.0
        } else {
            (self.hits as f64 / total as f64) * 100.0
        }
    }
}

/// Internal trait for cache operations
pub(crate) trait Cache: Send + Sync {
    /// Get a value from the cache
    fn get(&self, key: &str) -> Option<u64>;

    /// Put a value into the cache
    fn put(&self, key: String, value: u64);

    /// Get cache statistics
    fn stats(&self) -> CacheStats;
}

/// LRU cache wrapper with metrics tracking
pub(crate) struct LruCacheWrapper {
    cache: Arc<RwLock<LruCache<String, u64>>>,
    capacity: usize,
    hits: Arc<AtomicU64>,
    misses: Arc<AtomicU64>,
}

impl LruCacheWrapper {
    pub fn new(capacity: usize) -> Self {
        let capacity_nz = NonZeroUsize::new(capacity).expect("Cache capacity must be non-zero");
        Self {
            cache: Arc::new(RwLock::new(LruCache::new(capacity_nz))),
            capacity,
            hits: Arc::new(AtomicU64::new(0)),
            misses: Arc::new(AtomicU64::new(0)),
        }
    }
}

impl Cache for LruCacheWrapper {
    fn get(&self, key: &str) -> Option<u64> {
        if let Ok(mut cache) = self.cache.write() {
            let result = cache.get(key).copied();
            if result.is_some() {
                self.hits.fetch_add(1, Ordering::Relaxed);
            } else {
                self.misses.fetch_add(1, Ordering::Relaxed);
            }
            result
        } else {
            None
        }
    }

    fn put(&self, key: String, value: u64) {
        if let Ok(mut cache) = self.cache.write() {
            cache.put(key, value);
        }
    }

    fn stats(&self) -> CacheStats {
        let entry_count = if let Ok(cache) = self.cache.read() {
            cache.len() as u64
        } else {
            0
        };

        CacheStats {
            hits: self.hits.load(Ordering::Relaxed),
            misses: self.misses.load(Ordering::Relaxed),
            entry_count,
            capacity: self.capacity as u64,
        }
    }
}

/// Moka (W-TinyLFU) cache wrapper with metrics tracking
pub(crate) struct MokaCacheWrapper {
    cache: MokaCache<String, u64>,
    capacity: usize,
    hits: Arc<AtomicU64>,
    misses: Arc<AtomicU64>,
}

impl MokaCacheWrapper {
    pub fn new(capacity: usize) -> Self {
        Self {
            cache: MokaCache::builder().max_capacity(capacity as u64).build(),
            capacity,
            hits: Arc::new(AtomicU64::new(0)),
            misses: Arc::new(AtomicU64::new(0)),
        }
    }
}

impl Cache for MokaCacheWrapper {
    fn get(&self, key: &str) -> Option<u64> {
        let result = self.cache.get(key);
        if result.is_some() {
            self.hits.fetch_add(1, Ordering::Relaxed);
        } else {
            self.misses.fetch_add(1, Ordering::Relaxed);
        }
        result
    }

    fn put(&self, key: String, value: u64) {
        self.cache.insert(key, value);
    }

    fn stats(&self) -> CacheStats {
        let hits = self.hits.load(Ordering::Relaxed);
        let misses = self.misses.load(Ordering::Relaxed);
        let entry_count = self.cache.entry_count();

        CacheStats {
            hits,
            misses,
            entry_count,
            capacity: self.capacity as u64,
        }
    }
}

/// Create a cache instance based on the strategy
pub(crate) fn create_cache(strategy: CacheStrategy, capacity: usize) -> Arc<dyn Cache> {
    match strategy {
        CacheStrategy::Lru => Arc::new(LruCacheWrapper::new(capacity)),
        CacheStrategy::Moka => Arc::new(MokaCacheWrapper::new(capacity)),
    }
}