//! Caching layer for Synor storage. //! //! Provides LRU caches for frequently accessed data to reduce disk I/O. //! //! OPTIMIZATION: Uses the `lru` crate for O(1) get/put operations instead of //! the O(n) Vec-based implementation. This significantly improves performance //! for high-throughput scenarios. use parking_lot::RwLock; use std::hash::Hash; use std::num::NonZeroUsize; use synor_types::{BlockHeader, BlockId, Hash256, Transaction, TransactionId}; use crate::stores::{StoredGhostdagData, StoredRelations, StoredUtxo}; /// A wrapper around the `lru` crate's LruCache with our API. /// /// This provides O(1) get and put operations using a doubly-linked list /// with a HashMap for fast lookups. pub struct LruCache { inner: lru::LruCache, } impl LruCache { /// Creates a new LRU cache with the given capacity. pub fn new(capacity: usize) -> Self { let cap = NonZeroUsize::new(capacity.max(1)).unwrap(); LruCache { inner: lru::LruCache::new(cap), } } /// Gets a value from the cache, updating access order. /// O(1) operation. pub fn get(&mut self, key: &K) -> Option { self.inner.get(key).cloned() } /// Peeks at a value without updating access order. /// O(1) operation. pub fn peek(&self, key: &K) -> Option<&V> { self.inner.peek(key) } /// Inserts a value into the cache. /// O(1) operation. Returns the evicted value if cache was at capacity. pub fn insert(&mut self, key: K, value: V) -> Option { self.inner.push(key, value).map(|(_, v)| v) } /// Removes a value from the cache. /// O(1) operation. pub fn remove(&mut self, key: &K) -> Option { self.inner.pop(key) } /// Returns the number of entries in the cache. pub fn len(&self) -> usize { self.inner.len() } /// Returns true if the cache is empty. pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Clears the cache. pub fn clear(&mut self) { self.inner.clear(); } /// Returns true if the key is in the cache. pub fn contains(&self, key: &K) -> bool { self.inner.contains(key) } /// Returns the capacity of the cache. pub fn capacity(&self) -> usize { self.inner.cap().get() } } /// Configuration for the storage cache. #[derive(Clone, Debug)] pub struct CacheConfig { /// Maximum number of headers to cache. pub header_cache_size: usize, /// Maximum number of block bodies to cache. pub block_cache_size: usize, /// Maximum number of transactions to cache. pub tx_cache_size: usize, /// Maximum number of UTXOs to cache. pub utxo_cache_size: usize, /// Maximum number of GHOSTDAG entries to cache. pub ghostdag_cache_size: usize, /// Maximum number of relations to cache. pub relations_cache_size: usize, } impl Default for CacheConfig { fn default() -> Self { CacheConfig { header_cache_size: 10_000, block_cache_size: 1_000, tx_cache_size: 50_000, utxo_cache_size: 100_000, ghostdag_cache_size: 50_000, relations_cache_size: 50_000, } } } impl CacheConfig { /// Creates a configuration for low-memory environments. pub fn low_memory() -> Self { CacheConfig { header_cache_size: 1_000, block_cache_size: 100, tx_cache_size: 5_000, utxo_cache_size: 10_000, ghostdag_cache_size: 5_000, relations_cache_size: 5_000, } } /// Creates a configuration for high-memory environments. pub fn high_memory() -> Self { CacheConfig { header_cache_size: 100_000, block_cache_size: 10_000, tx_cache_size: 500_000, utxo_cache_size: 1_000_000, ghostdag_cache_size: 500_000, relations_cache_size: 500_000, } } } /// UTXO cache key (txid + index). #[derive(Clone, Hash, Eq, PartialEq)] pub struct UtxoKey { pub txid: TransactionId, pub index: u32, } impl UtxoKey { pub fn new(txid: TransactionId, index: u32) -> Self { UtxoKey { txid, index } } } /// Cache statistics. #[derive(Clone, Debug, Default)] pub struct CacheStats { /// Number of cache hits. pub hits: u64, /// Number of cache misses. pub misses: u64, /// Number of insertions. pub insertions: u64, /// Number of evictions. pub evictions: u64, } impl CacheStats { /// Returns the hit ratio (0.0 to 1.0). pub fn hit_ratio(&self) -> f64 { let total = self.hits + self.misses; if total == 0 { 0.0 } else { self.hits as f64 / total as f64 } } } /// Combined storage cache for all data types. pub struct StorageCache { /// Header cache. headers: RwLock>, /// Transaction cache. transactions: RwLock>, /// UTXO cache. utxos: RwLock>, /// GHOSTDAG data cache. ghostdag: RwLock>, /// Relations cache. relations: RwLock>, /// Cache statistics. stats: RwLock, /// Configuration. config: CacheConfig, } impl StorageCache { /// Creates a new storage cache with the given configuration. pub fn new(config: CacheConfig) -> Self { StorageCache { headers: RwLock::new(LruCache::new(config.header_cache_size)), transactions: RwLock::new(LruCache::new(config.tx_cache_size)), utxos: RwLock::new(LruCache::new(config.utxo_cache_size)), ghostdag: RwLock::new(LruCache::new(config.ghostdag_cache_size)), relations: RwLock::new(LruCache::new(config.relations_cache_size)), stats: RwLock::new(CacheStats::default()), config, } } /// Creates a new storage cache with default configuration. pub fn default_config() -> Self { Self::new(CacheConfig::default()) } // Header cache operations /// Gets a header from the cache. pub fn get_header(&self, hash: &Hash256) -> Option { let mut cache = self.headers.write(); let result = cache.get(hash); let mut stats = self.stats.write(); if result.is_some() { stats.hits += 1; } else { stats.misses += 1; } result } /// Inserts a header into the cache. pub fn insert_header(&self, header: BlockHeader) { let hash = header.block_id(); let mut cache = self.headers.write(); let was_full = cache.len() >= cache.capacity(); cache.insert(hash, header); let mut stats = self.stats.write(); stats.insertions += 1; if was_full { stats.evictions += 1; } } /// Removes a header from the cache. pub fn remove_header(&self, hash: &Hash256) { let mut cache = self.headers.write(); cache.remove(hash); } // Transaction cache operations /// Gets a transaction from the cache. pub fn get_transaction(&self, txid: &TransactionId) -> Option { let mut cache = self.transactions.write(); let result = cache.get(txid); let mut stats = self.stats.write(); if result.is_some() { stats.hits += 1; } else { stats.misses += 1; } result } /// Inserts a transaction into the cache. pub fn insert_transaction(&self, tx: Transaction) { let txid = tx.txid(); let mut cache = self.transactions.write(); cache.insert(txid, tx); self.stats.write().insertions += 1; } /// Removes a transaction from the cache. pub fn remove_transaction(&self, txid: &TransactionId) { let mut cache = self.transactions.write(); cache.remove(txid); } // UTXO cache operations /// Gets a UTXO from the cache. pub fn get_utxo(&self, txid: &TransactionId, index: u32) -> Option { let key = UtxoKey::new(*txid, index); let mut cache = self.utxos.write(); let result = cache.get(&key); let mut stats = self.stats.write(); if result.is_some() { stats.hits += 1; } else { stats.misses += 1; } result } /// Inserts a UTXO into the cache. pub fn insert_utxo(&self, txid: TransactionId, index: u32, utxo: StoredUtxo) { let key = UtxoKey::new(txid, index); let mut cache = self.utxos.write(); cache.insert(key, utxo); self.stats.write().insertions += 1; } /// Removes a UTXO from the cache. pub fn remove_utxo(&self, txid: &TransactionId, index: u32) { let key = UtxoKey::new(*txid, index); let mut cache = self.utxos.write(); cache.remove(&key); } // GHOSTDAG cache operations /// Gets GHOSTDAG data from the cache. pub fn get_ghostdag(&self, block_id: &BlockId) -> Option { let mut cache = self.ghostdag.write(); let result = cache.get(block_id); let mut stats = self.stats.write(); if result.is_some() { stats.hits += 1; } else { stats.misses += 1; } result } /// Inserts GHOSTDAG data into the cache. pub fn insert_ghostdag(&self, block_id: BlockId, data: StoredGhostdagData) { let mut cache = self.ghostdag.write(); cache.insert(block_id, data); self.stats.write().insertions += 1; } /// Removes GHOSTDAG data from the cache. pub fn remove_ghostdag(&self, block_id: &BlockId) { let mut cache = self.ghostdag.write(); cache.remove(block_id); } // Relations cache operations /// Gets relations from the cache. pub fn get_relations(&self, block_id: &BlockId) -> Option { let mut cache = self.relations.write(); let result = cache.get(block_id); let mut stats = self.stats.write(); if result.is_some() { stats.hits += 1; } else { stats.misses += 1; } result } /// Inserts relations into the cache. pub fn insert_relations(&self, block_id: BlockId, relations: StoredRelations) { let mut cache = self.relations.write(); cache.insert(block_id, relations); self.stats.write().insertions += 1; } /// Removes relations from the cache. pub fn remove_relations(&self, block_id: &BlockId) { let mut cache = self.relations.write(); cache.remove(block_id); } // Statistics and management /// Returns cache statistics. pub fn stats(&self) -> CacheStats { self.stats.read().clone() } /// Resets cache statistics. pub fn reset_stats(&self) { *self.stats.write() = CacheStats::default(); } /// Returns the total number of cached entries. pub fn total_entries(&self) -> usize { self.headers.read().len() + self.transactions.read().len() + self.utxos.read().len() + self.ghostdag.read().len() + self.relations.read().len() } /// Clears all caches. pub fn clear(&self) { self.headers.write().clear(); self.transactions.write().clear(); self.utxos.write().clear(); self.ghostdag.write().clear(); self.relations.write().clear(); } /// Returns the configuration. pub fn config(&self) -> &CacheConfig { &self.config } /// Returns cache size information. pub fn size_info(&self) -> CacheSizeInfo { CacheSizeInfo { headers: self.headers.read().len(), headers_capacity: self.config.header_cache_size, transactions: self.transactions.read().len(), transactions_capacity: self.config.tx_cache_size, utxos: self.utxos.read().len(), utxos_capacity: self.config.utxo_cache_size, ghostdag: self.ghostdag.read().len(), ghostdag_capacity: self.config.ghostdag_cache_size, relations: self.relations.read().len(), relations_capacity: self.config.relations_cache_size, } } } impl Default for StorageCache { fn default() -> Self { Self::default_config() } } /// Cache size information. #[derive(Clone, Debug)] pub struct CacheSizeInfo { pub headers: usize, pub headers_capacity: usize, pub transactions: usize, pub transactions_capacity: usize, pub utxos: usize, pub utxos_capacity: usize, pub ghostdag: usize, pub ghostdag_capacity: usize, pub relations: usize, pub relations_capacity: usize, } impl CacheSizeInfo { /// Returns the total number of cached entries. pub fn total_entries(&self) -> usize { self.headers + self.transactions + self.utxos + self.ghostdag + self.relations } /// Returns the total capacity. pub fn total_capacity(&self) -> usize { self.headers_capacity + self.transactions_capacity + self.utxos_capacity + self.ghostdag_capacity + self.relations_capacity } /// Returns the overall fill ratio. pub fn fill_ratio(&self) -> f64 { self.total_entries() as f64 / self.total_capacity() as f64 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_lru_cache_basic() { let mut cache: LruCache = LruCache::new(3); cache.insert(1, "one".to_string()); cache.insert(2, "two".to_string()); cache.insert(3, "three".to_string()); assert_eq!(cache.get(&1), Some("one".to_string())); assert_eq!(cache.get(&2), Some("two".to_string())); assert_eq!(cache.len(), 3); } #[test] fn test_lru_cache_eviction() { let mut cache: LruCache = LruCache::new(3); cache.insert(1, "one".to_string()); cache.insert(2, "two".to_string()); cache.insert(3, "three".to_string()); cache.insert(4, "four".to_string()); // Should evict 1 assert_eq!(cache.get(&1), None); assert_eq!(cache.get(&4), Some("four".to_string())); assert_eq!(cache.len(), 3); } #[test] fn test_lru_cache_access_order() { let mut cache: LruCache = LruCache::new(3); cache.insert(1, "one".to_string()); cache.insert(2, "two".to_string()); cache.insert(3, "three".to_string()); // Access 1, making it most recently used cache.get(&1); // Insert 4, should evict 2 (least recently used) cache.insert(4, "four".to_string()); assert_eq!(cache.get(&1), Some("one".to_string())); assert_eq!(cache.get(&2), None); assert_eq!(cache.get(&3), Some("three".to_string())); assert_eq!(cache.get(&4), Some("four".to_string())); } #[test] fn test_storage_cache_stats() { let cache = StorageCache::new(CacheConfig { header_cache_size: 10, block_cache_size: 10, tx_cache_size: 10, utxo_cache_size: 10, ghostdag_cache_size: 10, relations_cache_size: 10, }); // Miss let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0); assert_eq!(cache.stats().misses, 1); // Insert and hit cache.insert_utxo( TransactionId::from_bytes([1u8; 32]), 0, StoredUtxo { amount: 1000, script_pubkey: vec![], block_daa_score: 0, is_coinbase: false, }, ); let _ = cache.get_utxo(&TransactionId::from_bytes([1u8; 32]), 0); let stats = cache.stats(); assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 1); assert_eq!(stats.insertions, 1); } #[test] fn test_cache_size_info() { let cache = StorageCache::default_config(); let info = cache.size_info(); assert_eq!(info.total_entries(), 0); assert!(info.total_capacity() > 0); assert_eq!(info.fill_ratio(), 0.0); } }