Skip to main content

nostro2_cache/
lib.rs

1use std::sync::{Arc, Mutex};
2
3/// Event ID deduplication cache using std::sync::Mutex with LRU eviction
4///
5/// This is the winning strategy from benchmarks - fastest under realistic
6/// multi-threaded relay pool scenarios (10-20 concurrent connections).
7///
8/// Pros:
9/// - Automatic LRU eviction, bounded memory
10/// - Excellent performance under realistic concurrency
11/// - Zero external dependencies beyond lru crate
12/// - Simple, predictable behavior
13pub struct Cache {
14    cache: Arc<Mutex<lru::LruCache<String, ()>>>,
15}
16
17impl Cache {
18    /// Create a new cache with the specified capacity
19    ///
20    /// # Arguments
21    /// * `capacity` - Maximum number of event IDs to cache
22    ///
23    /// # Example
24    /// ```
25    /// use nostro2_cache::Cache;
26    ///
27    /// let cache = Cache::new(10_000);
28    /// ```
29    pub fn new(capacity: usize) -> Self {
30        Self {
31            cache: Arc::new(Mutex::new(lru::LruCache::new(
32                std::num::NonZeroUsize::new(capacity).unwrap(),
33            ))),
34        }
35    }
36
37    /// Insert an event ID into the cache
38    ///
39    /// Returns `true` if this is a new event (not seen before),
40    /// `false` if the event was already in the cache (duplicate).
41    ///
42    /// # Example
43    /// ```
44    /// use nostro2_cache::Cache;
45    ///
46    /// let cache = Cache::new(10_000);
47    ///
48    /// if cache.insert("event_id_123".to_string()) {
49    ///     println!("New event!");
50    /// } else {
51    ///     println!("Duplicate, skip");
52    /// }
53    /// ```
54    pub fn insert(&self, id: String) -> bool {
55        let mut cache = self.cache.lock().unwrap();
56        cache.put(id, ()).is_none()
57    }
58
59    /// Check if the cache contains an event ID
60    pub fn contains(&self, id: &str) -> bool {
61        let mut cache = self.cache.lock().unwrap();
62        cache.get(id).is_some()
63    }
64
65    /// Get the current number of cached event IDs
66    pub fn len(&self) -> usize {
67        self.cache.lock().unwrap().len()
68    }
69
70    /// Check if the cache is empty
71    pub fn is_empty(&self) -> bool {
72        self.cache.lock().unwrap().is_empty()
73    }
74}
75
76impl Clone for Cache {
77    fn clone(&self) -> Self {
78        Self {
79            cache: Arc::clone(&self.cache),
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_cache_basic() {
90        let cache = Cache::new(10);
91        assert!(cache.insert("id1".to_string()));
92        assert!(!cache.insert("id1".to_string())); // Duplicate
93        assert!(cache.contains("id1"));
94    }
95
96    #[test]
97    fn test_cache_lru_eviction() {
98        let cache = Cache::new(3);
99
100        // Fill cache
101        cache.insert("id1".to_string());
102        cache.insert("id2".to_string());
103        cache.insert("id3".to_string());
104
105        // Insert 4th item, should evict oldest (id1)
106        cache.insert("id4".to_string());
107
108        assert!(!cache.contains("id1")); // Evicted
109        assert!(cache.contains("id2"));
110        assert!(cache.contains("id3"));
111        assert!(cache.contains("id4"));
112    }
113
114    #[test]
115    fn test_cache_len() {
116        let cache = Cache::new(10);
117        assert_eq!(cache.len(), 0);
118
119        cache.insert("id1".to_string());
120        assert_eq!(cache.len(), 1);
121
122        cache.insert("id1".to_string()); // Duplicate
123        assert_eq!(cache.len(), 1);
124
125        cache.insert("id2".to_string());
126        assert_eq!(cache.len(), 2);
127    }
128}