1use std::collections::HashMap;
10use std::hash::Hash;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13use tokio::sync::RwLock;
14
15#[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
37#[derive(Debug, Clone, Default)]
39pub struct CacheStats {
40 pub hits: u64,
42 pub misses: u64,
44 pub sets: u64,
46 pub evictions: u64,
48 pub entries: usize,
50}
51
52impl CacheStats {
53 pub fn hit_rate(&self) -> f64 {
55 let total = self.hits + self.misses;
56 if total == 0 {
57 0.0
58 } else {
59 (self.hits as f64 / total as f64) * 100.0
60 }
61 }
62}
63
64pub struct Cache<K, V>
66where
67 K: Eq + Hash + Clone,
68 V: Clone,
69{
70 store: Arc<RwLock<HashMap<K, CacheEntry<V>>>>,
71 max_size: usize,
72 stats: Arc<RwLock<CacheStats>>,
73}
74
75impl<K, V> Cache<K, V>
76where
77 K: Eq + Hash + Clone,
78 V: Clone,
79{
80 pub fn new(max_size: usize) -> Self {
82 Self {
83 store: Arc::new(RwLock::new(HashMap::new())),
84 max_size,
85 stats: Arc::new(RwLock::new(CacheStats::default())),
86 }
87 }
88
89 pub async fn get(&self, key: &K) -> Option<V> {
91 let store = self.store.read().await;
92 let mut stats = self.stats.write().await;
93
94 if let Some(entry) = store.get(key) {
95 if !entry.is_expired() {
96 stats.hits += 1;
97 return Some(entry.value.clone());
98 }
99 }
100
101 stats.misses += 1;
102 None
103 }
104
105 pub async fn set(&self, key: K, value: V, ttl: Duration) {
107 let mut store = self.store.write().await;
108 let mut stats = self.stats.write().await;
109
110 if store.len() >= self.max_size && !store.contains_key(&key) {
112 let expired_keys: Vec<K> = store
114 .iter()
115 .filter(|(_, entry)| entry.is_expired())
116 .map(|(k, _)| k.clone())
117 .collect();
118
119 for k in expired_keys {
120 store.remove(&k);
121 stats.evictions += 1;
122 }
123
124 if store.len() >= self.max_size {
126 if let Some(oldest_key) = store
127 .iter()
128 .min_by_key(|(_, entry)| entry.inserted_at)
129 .map(|(k, _)| k.clone())
130 {
131 store.remove(&oldest_key);
132 stats.evictions += 1;
133 }
134 }
135 }
136
137 store.insert(key, CacheEntry::new(value, ttl));
138 stats.sets += 1;
139 stats.entries = store.len();
140 }
141
142 pub async fn remove(&self, key: &K) -> Option<V> {
144 let mut store = self.store.write().await;
145 let mut stats = self.stats.write().await;
146
147 if let Some(entry) = store.remove(key) {
148 stats.entries = store.len();
149 Some(entry.value)
150 } else {
151 None
152 }
153 }
154
155 pub async fn clear(&self) {
157 let mut store = self.store.write().await;
158 let mut stats = self.stats.write().await;
159
160 store.clear();
161 stats.entries = 0;
162 }
163
164 pub async fn stats(&self) -> CacheStats {
166 self.stats.read().await.clone()
167 }
168
169 pub async fn cleanup_expired(&self) {
171 let mut store = self.store.write().await;
172 let mut stats = self.stats.write().await;
173
174 let expired_keys: Vec<K> = store
175 .iter()
176 .filter(|(_, entry)| entry.is_expired())
177 .map(|(k, _)| k.clone())
178 .collect();
179
180 let count = expired_keys.len();
181 for k in expired_keys {
182 store.remove(&k);
183 }
184
185 if count > 0 {
186 tracing::debug!("Cleaned up {} expired cache entries", count);
187 stats.evictions += count as u64;
188 stats.entries = store.len();
189 }
190 }
191
192 pub async fn len(&self) -> usize {
194 self.store.read().await.len()
195 }
196
197 pub async fn is_empty(&self) -> bool {
199 self.store.read().await.is_empty()
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct CacheConfig {
206 pub balance_ttl_secs: u64,
208 pub transaction_status_ttl_secs: u64,
210 pub block_data_ttl_secs: u64,
212 pub chain_metadata_ttl_secs: u64,
214 pub max_cache_size: usize,
216 pub cleanup_interval_secs: u64,
218}
219
220impl Default for CacheConfig {
221 fn default() -> Self {
222 Self {
223 balance_ttl_secs: 30,
224 transaction_status_ttl_secs: 300,
225 block_data_ttl_secs: 3600,
226 chain_metadata_ttl_secs: 3600,
227 max_cache_size: 10000,
228 cleanup_interval_secs: 300,
229 }
230 }
231}
232
233pub struct EvmCache {
235 balance_cache: Cache<String, String>,
236 tx_status_cache: Cache<String, String>,
237 block_cache: Cache<u64, String>,
238 config: CacheConfig,
239}
240
241impl EvmCache {
242 pub fn new() -> Self {
244 Self::with_config(CacheConfig::default())
245 }
246
247 pub fn with_config(config: CacheConfig) -> Self {
249 Self {
250 balance_cache: Cache::new(config.max_cache_size),
251 tx_status_cache: Cache::new(config.max_cache_size),
252 block_cache: Cache::new(config.max_cache_size / 10), config,
254 }
255 }
256
257 pub async fn get_balance(&self, address: &str) -> Option<String> {
259 self.balance_cache.get(&address.to_string()).await
260 }
261
262 pub async fn set_balance(&self, address: &str, balance: String) {
264 let ttl = Duration::from_secs(self.config.balance_ttl_secs);
265 self.balance_cache
266 .set(address.to_string(), balance, ttl)
267 .await;
268 }
269
270 pub async fn get_tx_status(&self, tx_hash: &str) -> Option<String> {
272 self.tx_status_cache.get(&tx_hash.to_string()).await
273 }
274
275 pub async fn set_tx_status(&self, tx_hash: &str, status: String) {
277 let ttl = Duration::from_secs(self.config.transaction_status_ttl_secs);
278 self.tx_status_cache
279 .set(tx_hash.to_string(), status, ttl)
280 .await;
281 }
282
283 pub async fn get_block(&self, block_number: u64) -> Option<String> {
285 self.block_cache.get(&block_number).await
286 }
287
288 pub async fn set_block(&self, block_number: u64, data: String) {
290 let ttl = Duration::from_secs(self.config.block_data_ttl_secs);
291 self.block_cache.set(block_number, data, ttl).await;
292 }
293
294 pub async fn clear_all(&self) {
296 self.balance_cache.clear().await;
297 self.tx_status_cache.clear().await;
298 self.block_cache.clear().await;
299 tracing::info!("Cleared all caches");
300 }
301
302 pub async fn stats(&self) -> HashMap<String, CacheStats> {
304 let mut stats = HashMap::new();
305 stats.insert("balance".to_string(), self.balance_cache.stats().await);
306 stats.insert("tx_status".to_string(), self.tx_status_cache.stats().await);
307 stats.insert("block".to_string(), self.block_cache.stats().await);
308 stats
309 }
310
311 pub async fn cleanup(&self) {
313 self.balance_cache.cleanup_expired().await;
314 self.tx_status_cache.cleanup_expired().await;
315 self.block_cache.cleanup_expired().await;
316 }
317
318 pub fn start_cleanup_task(self: Arc<Self>) {
320 let cache = self.clone();
321 let interval = Duration::from_secs(self.config.cleanup_interval_secs);
322 let interval_secs = self.config.cleanup_interval_secs;
323
324 tokio::spawn(async move {
325 loop {
326 tokio::time::sleep(interval).await;
327 cache.cleanup().await;
328 }
329 });
330
331 tracing::info!(
332 "Started cache cleanup task with interval: {}s",
333 interval_secs
334 );
335 }
336}
337
338impl Default for EvmCache {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[tokio::test]
349 async fn test_cache_basic_operations() {
350 let cache: Cache<String, String> = Cache::new(100);
351
352 cache
354 .set(
355 "key1".to_string(),
356 "value1".to_string(),
357 Duration::from_secs(60),
358 )
359 .await;
360
361 let value = cache.get(&"key1".to_string()).await;
363 assert_eq!(value, Some("value1".to_string()));
364
365 let stats = cache.stats().await;
367 assert_eq!(stats.hits, 1);
368 assert_eq!(stats.sets, 1);
369 }
370
371 #[tokio::test]
372 async fn test_cache_expiration() {
373 let cache: Cache<String, String> = Cache::new(100);
374
375 cache
377 .set(
378 "key1".to_string(),
379 "value1".to_string(),
380 Duration::from_millis(100),
381 )
382 .await;
383
384 assert!(cache.get(&"key1".to_string()).await.is_some());
386
387 tokio::time::sleep(Duration::from_millis(200)).await;
389
390 assert!(cache.get(&"key1".to_string()).await.is_none());
392 }
393
394 #[tokio::test]
395 async fn test_cache_eviction() {
396 let cache: Cache<String, String> = Cache::new(2);
397
398 cache
400 .set(
401 "key1".to_string(),
402 "value1".to_string(),
403 Duration::from_secs(60),
404 )
405 .await;
406 cache
407 .set(
408 "key2".to_string(),
409 "value2".to_string(),
410 Duration::from_secs(60),
411 )
412 .await;
413
414 cache
416 .set(
417 "key3".to_string(),
418 "value3".to_string(),
419 Duration::from_secs(60),
420 )
421 .await;
422
423 let stats = cache.stats().await;
424 assert!(stats.evictions > 0);
425 }
426
427 #[tokio::test]
428 async fn test_evm_cache() {
429 let cache = EvmCache::new();
430
431 cache
433 .set_balance("0x123", "1000000000000000000".to_string())
434 .await;
435 let balance = cache.get_balance("0x123").await;
436 assert_eq!(balance, Some("1000000000000000000".to_string()));
437
438 cache.set_tx_status("0xabc", "confirmed".to_string()).await;
440 let status = cache.get_tx_status("0xabc").await;
441 assert_eq!(status, Some("confirmed".to_string()));
442
443 let stats = cache.stats().await;
445 assert!(stats.contains_key("balance"));
446 assert!(stats.contains_key("tx_status"));
447 }
448
449 #[test]
450 fn test_cache_stats_hit_rate() {
451 let stats = CacheStats {
452 hits: 80,
453 misses: 20,
454 ..Default::default()
455 };
456
457 assert_eq!(stats.hit_rate(), 80.0);
458 }
459}