apex_sdk_substrate/
cache.rs

1//! Caching layer for Substrate queries
2//!
3//! This module provides a caching layer for:
4//! - Storage queries
5//! - Account balances
6//! - Metadata
7//! - RPC responses
8
9use lru::LruCache;
10use parking_lot::RwLock;
11use std::num::NonZeroUsize;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14
15/// Cache entry with expiration
16#[derive(Clone)]
17struct CacheEntry<V> {
18    value: V,
19    inserted_at: Instant,
20    ttl: Duration,
21}
22
23impl<V> CacheEntry<V> {
24    fn new(value: V, ttl: Duration) -> Self {
25        Self {
26            value,
27            inserted_at: Instant::now(),
28            ttl,
29        }
30    }
31
32    fn is_expired(&self) -> bool {
33        self.inserted_at.elapsed() > self.ttl
34    }
35
36    fn get(&self) -> Option<&V> {
37        if self.is_expired() {
38            None
39        } else {
40            Some(&self.value)
41        }
42    }
43}
44
45/// Cache configuration
46#[derive(Debug, Clone)]
47pub struct CacheConfig {
48    /// Maximum number of entries per cache type
49    pub max_entries: usize,
50    /// Default TTL for storage queries
51    pub storage_ttl: Duration,
52    /// Default TTL for balance queries
53    pub balance_ttl: Duration,
54    /// Default TTL for metadata
55    pub metadata_ttl: Duration,
56    /// Default TTL for general RPC responses
57    pub rpc_ttl: Duration,
58    /// Enable cache statistics
59    pub enable_stats: bool,
60}
61
62impl Default for CacheConfig {
63    fn default() -> Self {
64        Self {
65            max_entries: 1000,
66            storage_ttl: Duration::from_secs(30),
67            balance_ttl: Duration::from_secs(10),
68            metadata_ttl: Duration::from_secs(300),
69            rpc_ttl: Duration::from_secs(60),
70            enable_stats: true,
71        }
72    }
73}
74
75impl CacheConfig {
76    /// Create a new cache configuration with defaults
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Set maximum entries
82    pub fn with_max_entries(mut self, max_entries: usize) -> Self {
83        self.max_entries = max_entries;
84        self
85    }
86
87    /// Set storage TTL
88    pub fn with_storage_ttl(mut self, ttl: Duration) -> Self {
89        self.storage_ttl = ttl;
90        self
91    }
92
93    /// Set balance TTL
94    pub fn with_balance_ttl(mut self, ttl: Duration) -> Self {
95        self.balance_ttl = ttl;
96        self
97    }
98
99    /// Set metadata TTL
100    pub fn with_metadata_ttl(mut self, ttl: Duration) -> Self {
101        self.metadata_ttl = ttl;
102        self
103    }
104
105    /// Set RPC TTL
106    pub fn with_rpc_ttl(mut self, ttl: Duration) -> Self {
107        self.rpc_ttl = ttl;
108        self
109    }
110}
111
112/// Multi-level cache for Substrate queries
113pub struct Cache {
114    config: CacheConfig,
115    storage_cache: Arc<RwLock<LruCache<String, CacheEntry<Vec<u8>>>>>,
116    balance_cache: Arc<RwLock<LruCache<String, CacheEntry<u128>>>>,
117    metadata_cache: Arc<RwLock<LruCache<String, CacheEntry<String>>>>,
118    rpc_cache: Arc<RwLock<LruCache<String, CacheEntry<String>>>>,
119    stats: Arc<RwLock<CacheStats>>,
120}
121
122impl Cache {
123    /// Create a new cache with default configuration
124    pub fn new() -> Self {
125        Self::with_config(CacheConfig::default())
126    }
127
128    /// Create a new cache with custom configuration
129    pub fn with_config(config: CacheConfig) -> Self {
130        let capacity = NonZeroUsize::new(config.max_entries)
131            .expect("CacheConfig.max_entries must be greater than 0");
132
133        Self {
134            storage_cache: Arc::new(RwLock::new(LruCache::new(capacity))),
135            balance_cache: Arc::new(RwLock::new(LruCache::new(capacity))),
136            metadata_cache: Arc::new(RwLock::new(LruCache::new(capacity))),
137            rpc_cache: Arc::new(RwLock::new(LruCache::new(capacity))),
138            stats: Arc::new(RwLock::new(CacheStats::default())),
139            config,
140        }
141    }
142
143    /// Get a storage value from cache
144    pub fn get_storage(&self, key: &str) -> Option<Vec<u8>> {
145        let mut cache = self.storage_cache.write();
146        if let Some(entry) = cache.get(key) {
147            if let Some(value) = entry.get() {
148                self.record_hit();
149                return Some(value.clone());
150            } else {
151                // Entry expired, remove it
152                cache.pop(key);
153            }
154        }
155        self.record_miss();
156        None
157    }
158
159    /// Put a storage value in cache
160    pub fn put_storage(&self, key: String, value: Vec<u8>) {
161        let entry = CacheEntry::new(value, self.config.storage_ttl);
162        self.storage_cache.write().put(key, entry);
163    }
164
165    /// Get a balance from cache
166    pub fn get_balance(&self, address: &str) -> Option<u128> {
167        let mut cache = self.balance_cache.write();
168        if let Some(entry) = cache.get(address) {
169            if let Some(value) = entry.get() {
170                self.record_hit();
171                return Some(*value);
172            } else {
173                cache.pop(address);
174            }
175        }
176        self.record_miss();
177        None
178    }
179
180    /// Put a balance in cache
181    pub fn put_balance(&self, address: String, balance: u128) {
182        let entry = CacheEntry::new(balance, self.config.balance_ttl);
183        self.balance_cache.write().put(address, entry);
184    }
185
186    /// Get metadata from cache
187    pub fn get_metadata(&self, key: &str) -> Option<String> {
188        let mut cache = self.metadata_cache.write();
189        if let Some(entry) = cache.get(key) {
190            if let Some(value) = entry.get() {
191                self.record_hit();
192                return Some(value.clone());
193            } else {
194                cache.pop(key);
195            }
196        }
197        self.record_miss();
198        None
199    }
200
201    /// Put metadata in cache
202    pub fn put_metadata(&self, key: String, metadata: String) {
203        let entry = CacheEntry::new(metadata, self.config.metadata_ttl);
204        self.metadata_cache.write().put(key, entry);
205    }
206
207    /// Get RPC response from cache
208    pub fn get_rpc(&self, key: &str) -> Option<String> {
209        let mut cache = self.rpc_cache.write();
210        if let Some(entry) = cache.get(key) {
211            if let Some(value) = entry.get() {
212                self.record_hit();
213                return Some(value.clone());
214            } else {
215                cache.pop(key);
216            }
217        }
218        self.record_miss();
219        None
220    }
221
222    /// Put RPC response in cache
223    pub fn put_rpc(&self, key: String, response: String) {
224        let entry = CacheEntry::new(response, self.config.rpc_ttl);
225        self.rpc_cache.write().put(key, entry);
226    }
227
228    /// Clear all caches
229    pub fn clear(&self) {
230        self.storage_cache.write().clear();
231        self.balance_cache.write().clear();
232        self.metadata_cache.write().clear();
233        self.rpc_cache.write().clear();
234        self.stats.write().reset();
235    }
236
237    /// Clear expired entries from all caches
238    pub fn clear_expired(&self) {
239        // Storage cache
240        {
241            let mut cache = self.storage_cache.write();
242            let keys: Vec<String> = cache
243                .iter()
244                .filter_map(|(k, v)| {
245                    if v.is_expired() {
246                        Some(k.clone())
247                    } else {
248                        None
249                    }
250                })
251                .collect();
252            for key in keys {
253                cache.pop(&key);
254            }
255        }
256
257        // Balance cache
258        {
259            let mut cache = self.balance_cache.write();
260            let keys: Vec<String> = cache
261                .iter()
262                .filter_map(|(k, v)| {
263                    if v.is_expired() {
264                        Some(k.clone())
265                    } else {
266                        None
267                    }
268                })
269                .collect();
270            for key in keys {
271                cache.pop(&key);
272            }
273        }
274
275        // Metadata cache
276        {
277            let mut cache = self.metadata_cache.write();
278            let keys: Vec<String> = cache
279                .iter()
280                .filter_map(|(k, v)| {
281                    if v.is_expired() {
282                        Some(k.clone())
283                    } else {
284                        None
285                    }
286                })
287                .collect();
288            for key in keys {
289                cache.pop(&key);
290            }
291        }
292
293        // RPC cache
294        {
295            let mut cache = self.rpc_cache.write();
296            let keys: Vec<String> = cache
297                .iter()
298                .filter_map(|(k, v)| {
299                    if v.is_expired() {
300                        Some(k.clone())
301                    } else {
302                        None
303                    }
304                })
305                .collect();
306            for key in keys {
307                cache.pop(&key);
308            }
309        }
310    }
311
312    /// Get cache statistics
313    pub fn stats(&self) -> CacheStats {
314        let mut stats = self.stats.read().clone();
315
316        // Add current size information
317        stats.storage_size = self.storage_cache.read().len();
318        stats.balance_size = self.balance_cache.read().len();
319        stats.metadata_size = self.metadata_cache.read().len();
320        stats.rpc_size = self.rpc_cache.read().len();
321
322        stats
323    }
324
325    /// Record a cache hit
326    fn record_hit(&self) {
327        if self.config.enable_stats {
328            self.stats.write().hits += 1;
329        }
330    }
331
332    /// Record a cache miss
333    fn record_miss(&self) {
334        if self.config.enable_stats {
335            self.stats.write().misses += 1;
336        }
337    }
338
339    /// Get total cache size
340    pub fn total_size(&self) -> usize {
341        self.storage_cache.read().len()
342            + self.balance_cache.read().len()
343            + self.metadata_cache.read().len()
344            + self.rpc_cache.read().len()
345    }
346}
347
348impl Default for Cache {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354/// Cache statistics
355#[derive(Debug, Clone, Default)]
356pub struct CacheStats {
357    /// Number of cache hits
358    pub hits: u64,
359    /// Number of cache misses
360    pub misses: u64,
361    /// Current storage cache size
362    pub storage_size: usize,
363    /// Current balance cache size
364    pub balance_size: usize,
365    /// Current metadata cache size
366    pub metadata_size: usize,
367    /// Current RPC cache size
368    pub rpc_size: usize,
369}
370
371impl CacheStats {
372    /// Calculate hit rate as a percentage
373    pub fn hit_rate(&self) -> f64 {
374        let total = self.hits + self.misses;
375        if total == 0 {
376            0.0
377        } else {
378            (self.hits as f64 / total as f64) * 100.0
379        }
380    }
381
382    /// Get total cache entries
383    pub fn total_entries(&self) -> usize {
384        self.storage_size + self.balance_size + self.metadata_size + self.rpc_size
385    }
386
387    /// Reset statistics
388    fn reset(&mut self) {
389        self.hits = 0;
390        self.misses = 0;
391        self.storage_size = 0;
392        self.balance_size = 0;
393        self.metadata_size = 0;
394        self.rpc_size = 0;
395    }
396}
397
398impl std::fmt::Display for CacheStats {
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        write!(
401            f,
402            "Cache Stats: {} hits, {} misses ({:.2}% hit rate), {} total entries",
403            self.hits,
404            self.misses,
405            self.hit_rate(),
406            self.total_entries()
407        )
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_cache_creation() {
417        let cache = Cache::new();
418        let stats = cache.stats();
419
420        assert_eq!(stats.hits, 0);
421        assert_eq!(stats.misses, 0);
422        assert_eq!(stats.total_entries(), 0);
423    }
424
425    #[test]
426    fn test_storage_cache() {
427        let cache = Cache::new();
428
429        // Miss
430        assert_eq!(cache.get_storage("key1"), None);
431
432        // Put
433        cache.put_storage("key1".to_string(), vec![1, 2, 3]);
434
435        // Hit
436        assert_eq!(cache.get_storage("key1"), Some(vec![1, 2, 3]));
437
438        let stats = cache.stats();
439        assert_eq!(stats.hits, 1);
440        assert_eq!(stats.misses, 1);
441        assert_eq!(stats.hit_rate(), 50.0);
442    }
443
444    #[test]
445    fn test_balance_cache() {
446        let cache = Cache::new();
447
448        cache.put_balance("addr1".to_string(), 1_000_000);
449        assert_eq!(cache.get_balance("addr1"), Some(1_000_000));
450
451        cache.put_balance("addr2".to_string(), 2_000_000);
452        assert_eq!(cache.get_balance("addr2"), Some(2_000_000));
453
454        let stats = cache.stats();
455        assert_eq!(stats.balance_size, 2);
456    }
457
458    #[test]
459    fn test_metadata_cache() {
460        let cache = Cache::new();
461
462        cache.put_metadata("pallet1".to_string(), "metadata".to_string());
463        assert_eq!(cache.get_metadata("pallet1"), Some("metadata".to_string()));
464    }
465
466    #[test]
467    fn test_rpc_cache() {
468        let cache = Cache::new();
469
470        cache.put_rpc("method1".to_string(), "response".to_string());
471        assert_eq!(cache.get_rpc("method1"), Some("response".to_string()));
472    }
473
474    #[test]
475    fn test_cache_clear() {
476        let cache = Cache::new();
477
478        cache.put_storage("key1".to_string(), vec![1, 2, 3]);
479        cache.put_balance("addr1".to_string(), 1_000_000);
480
481        assert!(cache.total_size() > 0);
482
483        cache.clear();
484
485        assert_eq!(cache.total_size(), 0);
486        let stats = cache.stats();
487        assert_eq!(stats.hits, 0);
488        assert_eq!(stats.misses, 0);
489    }
490
491    #[test]
492    fn test_cache_expiration() {
493        let config = CacheConfig::new().with_storage_ttl(Duration::from_millis(10));
494
495        let cache = Cache::with_config(config);
496
497        cache.put_storage("key1".to_string(), vec![1, 2, 3]);
498
499        // Should be cached
500        assert_eq!(cache.get_storage("key1"), Some(vec![1, 2, 3]));
501
502        // Wait for expiration
503        std::thread::sleep(Duration::from_millis(20));
504
505        // Should be expired
506        assert_eq!(cache.get_storage("key1"), None);
507    }
508
509    #[test]
510    fn test_lru_eviction() {
511        let config = CacheConfig::new().with_max_entries(2);
512        let cache = Cache::with_config(config);
513
514        cache.put_storage("key1".to_string(), vec![1]);
515        cache.put_storage("key2".to_string(), vec![2]);
516        cache.put_storage("key3".to_string(), vec![3]); // This should evict key1
517
518        // key1 should be evicted
519        let stats = cache.stats();
520        assert_eq!(stats.storage_size, 2);
521    }
522}