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}