Skip to main content

scirs2_core/
cache.rs

1//! Cache and memoization utilities for improved performance
2//!
3//! This module provides caching and memoization utilities to improve performance
4//! by avoiding redundant computations.
5
6use cached::{cached, Cached, LruTtlCache};
7use std::hash::Hash;
8use std::time::Duration;
9
10/// Cache configuration for the library
11#[derive(Debug, Clone, Copy)]
12pub struct CacheConfig {
13    /// Default cache size (number of items)
14    pub default_size: usize,
15    /// Default time-to-live for cached items (seconds)
16    pub default_ttl: u64,
17    /// Whether to enable caching by default
18    pub enable_caching: bool,
19}
20
21impl Default for CacheConfig {
22    fn default() -> Self {
23        Self {
24            default_size: 1024,
25            default_ttl: 3600, // 1 hour
26            enable_caching: true,
27        }
28    }
29}
30
31/// A sized cache with time-to-live (TTL) functionality
32///
33/// Internally backed by [`cached::LruTtlCache`]: entries are evicted once the
34/// configured size is exceeded (least-recently-used first), and any entry
35/// older than the configured TTL is treated as absent. An expired entry is
36/// physically removed from the store the next time it is looked up via
37/// [`Self::get`].
38pub struct TTLSizedCache<K, V>
39where
40    K: Hash + Eq + Clone,
41    V: Clone,
42{
43    /// Internal cache (LRU size bound + global TTL, both enforced by `cached`)
44    cache: LruTtlCache<K, V>,
45}
46
47impl<K, V> TTLSizedCache<K, V>
48where
49    K: Hash + Eq + Clone,
50    V: Clone,
51{
52    /// Create a new TTL cache with specified size and TTL
53    ///
54    /// `size` and `ttlseconds` are clamped to a minimum of `1`: the
55    /// underlying store requires both bounds to be non-zero, so this keeps
56    /// construction infallible while staying as close as possible to the
57    /// caller's request.
58    #[must_use]
59    pub fn new(size: usize, ttlseconds: u64) -> Self {
60        let size = size.max(1);
61        let ttl = Duration::from_secs(ttlseconds.max(1));
62        let cache = LruTtlCache::builder()
63            .max_size(size)
64            .ttl(ttl)
65            .build()
66            .expect(
67                "`size`/`ttlseconds` are clamped to be non-zero above, so \
68                 `LruTtlCache::builder().build()` can only fail here on allocator exhaustion",
69            );
70        Self { cache }
71    }
72
73    /// Insert a key-value pair into the cache
74    pub fn insert(&mut self, key: K, value: V) {
75        self.cache.cache_set(key, value);
76    }
77
78    /// Get a value from the cache if it exists and hasn't expired
79    #[must_use]
80    pub fn get(&mut self, key: &K) -> Option<V> {
81        self.cache.cache_get(key).cloned()
82    }
83
84    /// Remove a key-value pair from the cache
85    pub fn remove(&mut self, key: &K) {
86        self.cache.cache_remove(key);
87    }
88
89    /// Clear the cache
90    pub fn clear(&mut self) {
91        self.cache.cache_clear();
92    }
93
94    /// Get the number of items in the cache
95    #[must_use]
96    pub fn len(&self) -> usize {
97        self.cache.cache_size()
98    }
99
100    /// Check if the cache is empty
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.len() == 0
104    }
105}
106
107/// A thread-safe cache builder
108///
109/// This struct provides a fluent interface for configuring and building
110/// various types of caches.
111pub struct CacheBuilder {
112    /// Cache size
113    size: Option<usize>,
114    /// Time-to-live in seconds
115    ttl: Option<u64>,
116    /// Whether to make the cache thread-safe
117    thread_safe: bool,
118}
119
120impl Default for CacheBuilder {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl CacheBuilder {
127    /// Create a new cache builder
128    #[must_use]
129    pub fn new() -> Self {
130        Self {
131            size: None,
132            ttl: None,
133            thread_safe: false,
134        }
135    }
136
137    /// Set the cache size
138    #[must_use]
139    pub fn with_size(mut self, size: usize) -> Self {
140        self.size = Some(size);
141        self
142    }
143
144    /// Set the time-to-live in seconds
145    #[must_use]
146    pub fn with_ttl(mut self, ttl: u64) -> Self {
147        self.ttl = Some(ttl);
148        self
149    }
150
151    /// Make the cache thread-safe
152    #[must_use]
153    pub fn thread_safe(mut self) -> Self {
154        self.thread_safe = true;
155        self
156    }
157
158    /// Build a sized cache
159    #[must_use]
160    pub fn build_sized_cache<K, V>(self) -> TTLSizedCache<K, V>
161    where
162        K: Hash + Eq + Clone,
163        V: Clone,
164    {
165        let config = CacheConfig::default();
166        let size = self.size.unwrap_or(config.default_size);
167        let ttl = self.ttl.unwrap_or(config.default_ttl);
168
169        TTLSizedCache::new(size, ttl)
170    }
171}
172
173/// Example of how to use the cached attribute
174///
175/// ```ignore
176/// // Example disabled due to missing cached dependency
177/// use cached::cached;
178///
179/// #[cached(size = 100)]
180/// pub fn expensive_calculation(x: u64) -> u64 {
181///     // Expensive computation here
182///     x * x
183/// }
184/// ```
185/// Example of using TTL cache for memoization
186///
187/// This example shows how to use the TTL cache for memoizing expensive operations.
188///
189/// ```rust
190/// use scirs2_core::cache::TTLSizedCache;
191/// use std::time::Duration;
192///
193/// // Create a TTL cache
194/// let mut cache = TTLSizedCache::<String, String>::new(100, 60);
195///
196/// // Cache a value
197/// let key = "example_key";
198/// let value = "example_value";
199/// cache.insert(key.to_string(), value.to_string());
200///
201/// // Retrieve a value
202/// let retrieved_value = cache.get(&key.to_string());
203/// ```
204/// Compute Fibonacci numbers with memoization
205///
206/// This function demonstrates the use of memoization for computing
207/// Fibonacci numbers efficiently.
208///
209/// # Example
210///
211/// ```ignore
212/// use cached::cached;
213///
214/// #[cached(size = 100)]
215/// pub fn fibonacci_prime_cache(n: u64) -> u64 {
216///     match n {
217///         0 => 0,
218///         1 => 1,
219///         n => fibonacci_prime_cache(n - 1) + fibonacci_prime_cache(n - 2),
220///     }
221/// }
222/// ```
223///
224/// # Arguments
225///
226/// * `n` - The Fibonacci number to compute
227///
228/// # Returns
229///
230/// * The nth Fibonacci number
231#[cached]
232#[must_use]
233#[allow(dead_code)]
234pub fn fibonacci(n: u64) -> u64 {
235    match n {
236        0 => 0,
237        1 => 1,
238        n => fibonacci(n - 1) + fibonacci(n - 2),
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use std::thread;
246    use std::time::Instant;
247
248    #[test]
249    fn test_ttl_sized_cache() {
250        let mut cache = TTLSizedCache::<i32, &str>::new(5, 1);
251
252        // Test insertion and retrieval
253        cache.insert(1, "one");
254        cache.insert(2, "two");
255
256        assert_eq!(cache.get(&1), Some("one"));
257        assert_eq!(cache.get(&2), Some("two"));
258        assert_eq!(cache.get(&3), None);
259
260        // Test TTL expiration
261        thread::sleep(Duration::from_secs(2));
262
263        assert_eq!(cache.get(&1), None);
264        assert_eq!(cache.get(&2), None);
265
266        // Test size limit
267        for i in 0..10 {
268            cache.insert(i, "test");
269        }
270
271        // Only the last 5 should be in the cache due to size limit
272        for i in 0..5 {
273            assert_eq!(cache.get(&i), None);
274        }
275
276        for i in 5..10 {
277            assert_eq!(cache.get(&i), Some("test"));
278        }
279    }
280
281    #[test]
282    fn test_cache_builder() {
283        let cache = CacheBuilder::new()
284            .with_size(10)
285            .with_ttl(60)
286            .build_sized_cache::<String, i32>();
287
288        assert!(cache.is_empty());
289    }
290
291    #[test]
292    fn test_fibonacci() {
293        // Compute fibonacci numbers with memoization
294        let fib10 = fibonacci(10);
295        let fib20 = fibonacci(20);
296
297        assert_eq!(fib10, 55);
298        assert_eq!(fib20, 6765);
299
300        // The second call should be much faster due to memoization
301        let start = Instant::now();
302        let fib20_again = fibonacci(20);
303        let _elapsed = start.elapsed();
304
305        assert_eq!(fib20_again, 6765);
306
307        // The second call should be very fast (less than 1ms)
308        assert!(_elapsed.as_millis() < 10);
309    }
310}