Skip to main content

pubmed_client/
cache.rs

1use moka::future::Cache as MokaCache;
2use std::hash::Hash;
3use std::time::Duration;
4use tracing::{debug, info};
5
6use crate::pmc::models::PmcFullText;
7
8/// Configuration for memory cache
9#[derive(Debug, Clone)]
10pub struct CacheConfig {
11    /// Maximum number of items to store in memory cache
12    pub max_capacity: u64,
13    /// Time-to-live for cached items
14    pub time_to_live: Duration,
15}
16
17impl Default for CacheConfig {
18    fn default() -> Self {
19        Self {
20            max_capacity: 1000,
21            time_to_live: Duration::from_secs(7 * 24 * 60 * 60), // 7 days
22        }
23    }
24}
25
26/// Memory-only cache implementation using Moka
27#[derive(Clone)]
28pub struct MemoryCache<K, V> {
29    cache: MokaCache<K, V>,
30}
31
32impl<K, V> MemoryCache<K, V>
33where
34    K: Hash + Eq + Clone + Send + Sync + 'static,
35    V: Clone + Send + Sync + 'static,
36{
37    pub fn new(config: &CacheConfig) -> Self {
38        let cache = MokaCache::builder()
39            .max_capacity(config.max_capacity)
40            .time_to_live(config.time_to_live)
41            .build();
42
43        Self { cache }
44    }
45
46    pub async fn get(&self, key: &K) -> Option<V> {
47        let result = self.cache.get(key).await;
48        if result.is_some() {
49            debug!("Cache hit");
50        } else {
51            debug!("Cache miss");
52        }
53        result
54    }
55
56    pub async fn insert(&self, key: K, value: V) {
57        self.cache.insert(key, value).await;
58        info!("Item cached");
59    }
60
61    pub async fn clear(&self) {
62        self.cache.invalidate_all();
63        info!("Cache cleared");
64    }
65
66    pub fn entry_count(&self) -> u64 {
67        self.cache.entry_count()
68    }
69
70    pub async fn sync(&self) {
71        self.cache.run_pending_tasks().await;
72    }
73}
74
75/// Type alias for PMC cache
76pub type PmcCache = MemoryCache<String, PmcFullText>;
77
78/// Create a cache instance based on configuration
79pub fn create_cache(config: &CacheConfig) -> PmcCache {
80    MemoryCache::new(config)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[tokio::test]
88    async fn test_memory_cache_basic() {
89        let config = CacheConfig {
90            max_capacity: 10,
91            time_to_live: Duration::from_secs(60),
92        };
93        let cache = MemoryCache::<String, String>::new(&config);
94
95        // Test insert and get
96        cache.insert("key1".to_string(), "value1".to_string()).await;
97        assert_eq!(
98            cache.get(&"key1".to_string()).await,
99            Some("value1".to_string())
100        );
101
102        // Test cache miss
103        assert_eq!(cache.get(&"nonexistent".to_string()).await, None);
104
105        // Test clear
106        cache.clear().await;
107        assert_eq!(cache.get(&"key1".to_string()).await, None);
108    }
109
110    #[tokio::test]
111    async fn test_cache_entry_count() {
112        let config = CacheConfig::default();
113        let cache = MemoryCache::<String, String>::new(&config);
114
115        assert_eq!(cache.entry_count(), 0);
116
117        cache.insert("key1".to_string(), "value1".to_string()).await;
118        cache.sync().await;
119        assert_eq!(cache.entry_count(), 1);
120
121        cache.insert("key2".to_string(), "value2".to_string()).await;
122        cache.sync().await;
123        assert_eq!(cache.entry_count(), 2);
124
125        cache.clear().await;
126        cache.sync().await;
127        assert_eq!(cache.entry_count(), 0);
128    }
129}