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}