1use anyhow::Result;
10use rustc_hash::FxHashMap;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, RwLock};
13use std::time::{Duration, SystemTime};
14
15pub const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(120);
17
18pub const DEFAULT_MAX_CACHE_CAPACITY: usize = 1_000;
20
21pub const MAX_CONTEXT_ITEMS: usize = 5;
23
24pub trait CacheKey: Send + Sync + std::hash::Hash + Eq + Clone + 'static {
26 fn to_cache_key(&self) -> String;
27}
28
29pub trait CacheValue: Send + Sync + Clone + 'static {}
31
32impl<T> CacheValue for T where T: Send + Sync + Clone + 'static {}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct CacheStats {
37 pub hits: u64,
38 pub misses: u64,
39 pub evictions: u64,
40 pub current_size: usize,
41 pub max_size: usize,
42 pub total_memory_bytes: u64,
43}
44
45#[derive(Debug, Clone)]
47pub struct CacheEntry<V> {
48 pub value: Arc<V>,
49 pub created_at: SystemTime,
50 pub last_accessed: SystemTime,
51 pub access_count: u64,
52 pub size_bytes: u64,
53}
54
55impl<V> CacheEntry<V> {
56 pub fn new(value: V, size_bytes: u64) -> Self {
57 Self::from_arc(Arc::new(value), size_bytes)
58 }
59
60 pub fn from_arc(value: Arc<V>, size_bytes: u64) -> Self {
61 let now = SystemTime::now();
62 Self {
63 value,
64 created_at: now,
65 last_accessed: now,
66 access_count: 1,
67 size_bytes,
68 }
69 }
70
71 pub fn mark_accessed(&mut self) {
72 self.last_accessed = SystemTime::now();
73 self.access_count += 1;
74 }
75
76 pub fn is_expired(&self, ttl: Duration) -> bool {
77 SystemTime::now()
78 .duration_since(self.created_at)
79 .map(|age| age > ttl)
80 .unwrap_or(true)
81 }
82}
83
84pub struct UnifiedCache<K, V> {
89 inner: RwLock<UnifiedCacheInner<K, V>>,
90}
91
92struct UnifiedCacheInner<K, V> {
94 entries: FxHashMap<K, CacheEntry<V>>,
95 max_size: usize,
96 ttl: Duration,
97 stats: CacheStats,
98 eviction_policy: EvictionPolicy,
99}
100
101#[derive(Debug, Clone, Copy)]
102pub enum EvictionPolicy {
103 Lru,
105 Lfu,
107 Fifo,
109 TtlOnly,
111}
112
113impl<K, V> UnifiedCache<K, V>
114where
115 K: CacheKey,
116 V: CacheValue,
117{
118 pub fn new(max_size: usize, ttl: Duration, eviction_policy: EvictionPolicy) -> Self {
119 Self {
120 inner: RwLock::new(UnifiedCacheInner {
121 entries: FxHashMap::with_capacity_and_hasher(max_size, Default::default()),
122 max_size,
123 ttl,
124 stats: CacheStats { max_size, ..Default::default() },
125 eviction_policy,
126 }),
127 }
128 }
129
130 pub fn get(&self, key: &K) -> Option<Arc<V>> {
135 enum LookupState<T> {
136 Hit(Arc<T>),
137 Expired,
138 Miss,
139 }
140
141 let state = {
142 let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
143 let ttl = inner.ttl;
144
145 match inner.entries.get(key) {
146 Some(entry) if !entry.is_expired(ttl) => LookupState::Hit(Arc::clone(&entry.value)),
147 Some(_) => LookupState::Expired,
148 None => LookupState::Miss,
149 }
150 };
151
152 match state {
153 LookupState::Hit(value) => {
154 if let Ok(mut inner) = self.inner.try_write() {
155 if let Some(entry) = inner.entries.get_mut(key) {
156 entry.mark_accessed();
157 }
158 inner.stats.hits += 1;
159 }
160 Some(value)
161 }
162 LookupState::Expired => {
163 if let Ok(mut inner) = self.inner.try_write() {
164 let ttl = inner.ttl;
165 let should_remove = inner.entries.get(key).map(|entry| entry.is_expired(ttl)).unwrap_or(false);
166 if should_remove {
167 Self::remove_inner(&mut inner, key);
168 }
169 inner.stats.misses += 1;
170 }
171 None
172 }
173 LookupState::Miss => {
174 if let Ok(mut inner) = self.inner.try_write() {
175 inner.stats.misses += 1;
176 }
177 None
178 }
179 }
180 }
181
182 pub fn get_owned(&self, key: &K) -> Option<V> {
184 self.get(key).map(|arc| (*arc).clone())
185 }
186
187 pub fn insert(&self, key: K, value: V, size_bytes: u64) {
189 self.insert_entry(key, CacheEntry::new(value, size_bytes));
190 }
191
192 pub fn insert_arc(&self, key: K, value: Arc<V>, size_bytes: u64) {
194 self.insert_entry(key, CacheEntry::from_arc(value, size_bytes));
195 }
196
197 fn insert_entry(&self, key: K, entry: CacheEntry<V>) {
198 let Ok(mut inner) = self.inner.write() else {
199 return;
200 };
201
202 Self::remove_expired_entries_inner(&mut inner);
204
205 if inner.max_size == 0 {
207 return;
208 }
209
210 let size_bytes = entry.size_bytes;
211
212 if let Some(current) = inner.entries.get_mut(&key) {
215 let previous = std::mem::replace(current, entry);
216 inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_sub(previous.size_bytes);
217 inner.stats.current_size = inner.entries.len();
218 inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_add(size_bytes);
219 return;
220 }
221
222 if inner.entries.len() >= inner.max_size {
225 let to_remove = (inner.max_size / 10).max(1);
226 Self::evict_batch_inner(&mut inner, to_remove);
227 }
228
229 inner.entries.insert(key, entry);
230 inner.stats.current_size = inner.entries.len();
231 inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_add(size_bytes);
232 }
233
234 fn remove_expired_entries_inner(inner: &mut UnifiedCacheInner<K, V>) {
236 let expired_keys: Vec<K> = inner
237 .entries
238 .iter()
239 .filter_map(|(k, v)| if v.is_expired(inner.ttl) { Some(k.clone()) } else { None })
240 .collect();
241
242 for key in expired_keys {
243 Self::remove_inner(inner, &key);
244 }
245 }
246
247 fn evict_one_inner(inner: &mut UnifiedCacheInner<K, V>) {
249 if inner.entries.is_empty() {
250 return;
251 }
252
253 let key_to_remove = match inner.eviction_policy {
254 EvictionPolicy::Lru => Self::find_lru_entry_inner(inner),
255 EvictionPolicy::Lfu => Self::find_lfu_entry_inner(inner),
256 EvictionPolicy::Fifo => Self::find_fifo_entry_inner(inner),
257 EvictionPolicy::TtlOnly => Self::find_oldest_entry_inner(inner),
258 };
259
260 if let Some(key) = key_to_remove {
261 Self::remove_inner(inner, &key);
262 inner.stats.evictions += 1;
263 }
264 }
265
266 fn evict_batch_inner(inner: &mut UnifiedCacheInner<K, V>, count: usize) {
269 if inner.entries.is_empty() {
270 return;
271 }
272
273 let keys_to_remove: Vec<K> = match inner.eviction_policy {
274 EvictionPolicy::Lru => {
275 let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.last_accessed)).collect();
276 entries.sort_by_key(|(_, ts)| *ts);
277 entries.into_iter().take(count).map(|(k, _)| k).collect()
278 }
279 EvictionPolicy::Lfu => {
280 let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.access_count)).collect();
281 entries.sort_by_key(|(_, c)| *c);
282 entries.into_iter().take(count).map(|(k, _)| k).collect()
283 }
284 EvictionPolicy::Fifo | EvictionPolicy::TtlOnly => {
285 let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.created_at)).collect();
286 entries.sort_by_key(|(_, ts)| *ts);
287 entries.into_iter().take(count).map(|(k, _)| k).collect()
288 }
289 };
290
291 for key in &keys_to_remove {
292 Self::remove_inner(inner, key);
293 }
294 inner.stats.evictions += keys_to_remove.len() as u64;
295 }
296
297 fn find_lru_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
298 inner
299 .entries
300 .iter()
301 .min_by_key(|(_, entry)| entry.last_accessed)
302 .map(|(k, _)| k.clone())
303 }
304
305 fn find_lfu_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
306 inner
307 .entries
308 .iter()
309 .min_by_key(|(_, entry)| entry.access_count)
310 .map(|(k, _)| k.clone())
311 }
312
313 fn find_fifo_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
314 inner
315 .entries
316 .iter()
317 .min_by_key(|(_, entry)| entry.created_at)
318 .map(|(k, _)| k.clone())
319 }
320
321 fn find_oldest_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
322 Self::find_fifo_entry_inner(inner)
323 }
324
325 fn remove_inner(inner: &mut UnifiedCacheInner<K, V>, key: &K) {
326 if let Some(entry) = inner.entries.remove(key) {
327 inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_sub(entry.size_bytes);
328 inner.stats.current_size = inner.entries.len();
329 }
330 }
331
332 pub fn stats(&self) -> CacheStats {
334 self.inner.read().map(|inner| inner.stats.clone()).unwrap_or_default()
335 }
336
337 pub fn clear(&self) {
339 if let Ok(mut inner) = self.inner.write() {
340 inner.entries.clear();
341 inner.stats.current_size = 0;
342 inner.stats.total_memory_bytes = 0;
343 }
344 }
345
346 pub fn remove(&self, key: &K) {
348 let Ok(mut inner) = self.inner.write() else {
349 return;
350 };
351 Self::remove_inner(&mut inner, key);
352 }
353
354 pub fn len(&self) -> usize {
356 self.inner.read().map(|inner| inner.entries.len()).unwrap_or(0)
357 }
358
359 pub fn is_empty(&self) -> bool {
360 self.len() == 0
361 }
362
363 pub fn invalidate_prefix(&self, prefix: &str) {
372 self.remove_where(|key| key.to_cache_key().starts_with(prefix));
373 }
374
375 pub fn invalidate_path(&self, path: &str) {
384 self.invalidate_prefix(&format!("{path}:"));
385 }
386
387 pub fn invalidate_suffix(&self, suffix: &str) {
395 self.remove_where(|key| key.to_cache_key().ends_with(suffix));
396 }
397
398 pub fn invalidate_containing(&self, substring: &str) {
406 self.remove_where(|key| key.to_cache_key().contains(substring));
407 }
408
409 pub fn remove_where<F>(&self, mut predicate: F) -> usize
413 where
414 F: FnMut(&K) -> bool,
415 {
416 let Ok(mut inner) = self.inner.write() else {
417 return 0;
418 };
419
420 let keys_to_remove: Vec<K> = inner.entries.keys().filter(|key| predicate(key)).cloned().collect();
421
422 let removed_count = keys_to_remove.len();
423 for key in keys_to_remove {
424 Self::remove_inner(&mut inner, &key);
425 }
426 removed_count
427 }
428
429 pub fn total_memory_bytes(&self) -> u64 {
431 self.inner.read().map(|inner| inner.stats.total_memory_bytes).unwrap_or(0)
432 }
433
434 pub fn estimate_entry_cost(entry: &CacheEntry<V>) -> u64 {
437 let base_overhead: u64 = 100; let value_size = entry.size_bytes;
440 base_overhead + value_size
441 }
442
443 pub fn reduce_ttl(&self, factor: f64) -> Duration {
446 let Ok(mut inner) = self.inner.write() else {
447 return Duration::ZERO;
448 };
449 let new_ttl = Duration::from_secs_f64(inner.ttl.as_secs_f64() * factor);
450 inner.ttl = new_ttl;
451 new_ttl
452 }
453
454 pub fn evict_under_pressure(&self, target_reduction_percent: u32) -> usize {
461 let Ok(mut inner) = self.inner.write() else {
462 return 0;
463 };
464
465 let target_percent = std::cmp::min(100, target_reduction_percent);
467
468 let expired_before = inner.entries.len();
470 Self::remove_expired_entries_inner(&mut inner);
471 let expired_removed = expired_before - inner.entries.len();
472
473 let current_size = inner.entries.len();
475 let target_size = (current_size * (100 - target_percent) as usize) / 100;
476
477 let mut evicted_count = expired_removed;
479 while inner.entries.len() > target_size && !inner.entries.is_empty() {
480 Self::evict_one_inner(&mut inner);
481 evicted_count += 1;
482 }
483
484 evicted_count
485 }
486
487 pub fn clear_least_used(&self, percent_to_clear: u32) -> usize {
490 let Ok(mut inner) = self.inner.write() else {
491 return 0;
492 };
493
494 let percent = std::cmp::min(100, percent_to_clear);
495 let entries_to_remove = (inner.entries.len() * percent as usize) / 100;
496
497 let mut removed = 0usize;
498 for _ in 0..entries_to_remove {
499 if inner.entries.is_empty() {
500 break;
501 }
502 Self::evict_one_inner(&mut inner);
503 removed += 1;
504 }
505
506 removed
507 }
508
509 pub fn entries_by_usefulness(&self) -> Vec<(K, CacheEntry<V>)> {
512 let Ok(inner) = self.inner.read() else {
513 return Vec::new();
514 };
515
516 let now = SystemTime::now();
517 let mut entries: Vec<(K, CacheEntry<V>, u64)> = inner
518 .entries
519 .iter()
520 .map(|(k, entry)| {
521 let age_secs = now.duration_since(entry.last_accessed).unwrap_or_default().as_secs();
523
524 let recency_factor = std::cmp::max(1_u64, 3600 / (age_secs + 1));
526 let usefulness_score = entry.access_count * recency_factor;
527
528 (k.clone(), entry.clone(), usefulness_score)
529 })
530 .collect();
531
532 entries.sort_by_key(|(_, _, score)| std::cmp::Reverse(*score));
534 entries.into_iter().map(|(k, e, _)| (k, e)).collect()
535 }
536}
537
538pub fn estimate_json_size(value: &serde_json::Value) -> u64 {
540 match value {
541 serde_json::Value::Null => 4,
542 serde_json::Value::Bool(_) => 5,
543 serde_json::Value::Number(n) => n.to_string().len() as u64,
544 serde_json::Value::String(s) => s.len() as u64,
545 serde_json::Value::Array(arr) => arr
546 .iter()
547 .map(estimate_json_size)
548 .fold(0u64, |acc, size| acc.saturating_add(size)),
549 serde_json::Value::Object(obj) => obj
550 .iter()
551 .map(|(k, v)| (k.len() as u64).saturating_add(estimate_json_size(v)).saturating_add(3)) .fold(0u64, |acc, size| acc.saturating_add(size)),
553 }
554}
555
556pub fn create_cache_key<T: Serialize>(data: &T) -> Result<String> {
558 let json_bytes = serde_json::to_vec(data)?;
559
560 let mut hash = 0u64;
562 for (i, byte) in json_bytes.iter().enumerate() {
563 hash = hash.wrapping_mul(31).wrapping_add(*byte as u64);
564 hash = hash.rotate_left((i % 64) as u32);
565 }
566
567 Ok(format!("{hash:016x}"))
568}
569
570pub struct ContextAwareCache<K, V> {
572 inner: UnifiedCache<K, V>,
573}
574
575impl<K, V> ContextAwareCache<K, V>
576where
577 K: CacheKey,
578 V: CacheValue,
579{
580 pub fn new(max_size: usize, ttl: Duration, eviction_policy: EvictionPolicy) -> Self {
581 Self {
582 inner: UnifiedCache::new(max_size, ttl, eviction_policy),
583 }
584 }
585
586 pub fn get_context_limited<F>(&self, keys: &[K], mut process_fn: F) -> Vec<V>
588 where
589 F: FnMut(&K) -> Option<V>,
590 {
591 let mut results = Vec::with_capacity(MAX_CONTEXT_ITEMS.min(keys.len()));
592 let mut overflow_count = 0;
593
594 for key in keys {
595 if results.len() >= MAX_CONTEXT_ITEMS {
596 overflow_count += 1;
597 continue;
598 }
599
600 if let Some(value) = self.inner.get(key) {
601 results.push((*value).clone());
602 } else if let Some(value) = process_fn(key) {
603 let size = size_of_val(&value) as u64;
605 self.inner.insert(key.clone(), value.clone(), size);
606 results.push(value);
607 }
608 }
609
610 if overflow_count > 0 {
612 }
615
616 results
617 }
618
619 pub fn stats(&self) -> CacheStats {
620 self.inner.stats()
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
629 struct TestKey(String);
630
631 impl CacheKey for TestKey {
632 fn to_cache_key(&self) -> String {
633 self.0.clone()
634 }
635 }
636
637 #[test]
638 fn test_cache_basic_operations() {
639 let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
640 let key = TestKey("test".into());
641 let value: String = "test_value".into();
642
643 cache.insert(key.clone(), value.clone(), 100);
645 assert_eq!(*cache.get(&key).unwrap(), value);
646
647 let stats = cache.stats();
649 assert_eq!(stats.hits, 1);
650 assert_eq!(stats.misses, 0);
651 assert_eq!(stats.current_size, 1);
652 }
653
654 #[test]
655 fn insert_arc_preserves_value_arc_and_statistics() {
656 let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
657 let key = TestKey("arc".into());
658 let value = Arc::new("shared value".to_string());
659
660 cache.insert_arc(key.clone(), Arc::clone(&value), value.len() as u64);
661
662 let cached = cache.get(&key).expect("inserted value should be cached");
663 assert!(Arc::ptr_eq(&cached, &value));
664 assert_eq!(cache.stats().total_memory_bytes, value.len() as u64);
665 }
666
667 #[test]
668 fn replacing_entry_updates_memory_statistics() {
669 let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
670 let key = TestKey("replacement".into());
671
672 cache.insert(key.clone(), "small".to_string(), 5);
673 cache.insert(key, "larger replacement".to_string(), 18);
674
675 let stats = cache.stats();
676 assert_eq!(stats.current_size, 1);
677 assert_eq!(stats.total_memory_bytes, 18);
678 }
679
680 #[test]
681 fn replacing_entry_at_capacity_does_not_evict() {
682 let cache = UnifiedCache::new(1, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
683 let key = TestKey("replacement".into());
684
685 cache.insert(key.clone(), "old".to_string(), 3);
686 cache.insert(key.clone(), "new value".to_string(), 9);
687
688 assert_eq!(cache.get_owned(&key).as_deref(), Some("new value"));
689 let stats = cache.stats();
690 assert_eq!(stats.current_size, 1);
691 assert_eq!(stats.evictions, 0);
692 assert_eq!(stats.total_memory_bytes, 9);
693 }
694
695 #[test]
696 fn zero_capacity_cache_rejects_entries() {
697 let cache = UnifiedCache::new(0, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
698
699 cache.insert(TestKey("rejected".into()), "value".to_string(), 5);
700
701 assert_eq!(cache.len(), 0);
702 assert_eq!(cache.total_memory_bytes(), 0);
703 assert_eq!(cache.stats().evictions, 0);
704 }
705
706 #[test]
707 fn test_cache_expiration() {
708 let cache = UnifiedCache::new(10, Duration::from_millis(100), EvictionPolicy::Lru);
709 let key = TestKey("test".into());
710 let value: String = "test_value".into();
711
712 cache.insert(key.clone(), value, 100);
713 assert!(cache.get(&key).is_some());
714
715 std::thread::sleep(Duration::from_millis(150));
717 assert!(cache.get(&key).is_none());
718 }
719
720 #[test]
721 fn test_context_limited_cache() {
722 let cache = ContextAwareCache::new(100, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
723 let keys: Vec<TestKey> = (0..10).map(|i| TestKey(i.to_string())).collect();
724
725 let results = cache.get_context_limited(&keys, |key| Some(format!("value_{}", key.0)));
726
727 assert_eq!(results.len(), MAX_CONTEXT_ITEMS);
729 assert_eq!(results[0], "value_0");
730 assert_eq!(results[4], "value_4");
731 }
732
733 #[test]
734 fn test_pressure_aware_total_memory() {
735 let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
736
737 cache.insert(TestKey("k1".into()), "v1".to_string(), 100);
739 cache.insert(TestKey("k2".into()), "v2".to_string(), 200);
740 cache.insert(TestKey("k3".into()), "v3".to_string(), 300);
741
742 assert_eq!(cache.total_memory_bytes(), 600);
744 }
745
746 #[test]
747 fn test_pressure_aware_reduce_ttl() {
748 let cache: UnifiedCache<TestKey, String> = UnifiedCache::new(10, Duration::from_secs(300), EvictionPolicy::Lru);
749
750 let new_ttl = cache.reduce_ttl(0.4);
752 assert_eq!(new_ttl.as_secs(), 120); let new_ttl = cache.reduce_ttl(0.1);
756 assert_eq!(new_ttl.as_secs(), 12); }
758
759 #[test]
760 fn test_pressure_aware_evict_under_pressure() {
761 let cache: UnifiedCache<TestKey, String> =
762 UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
763
764 for i in 0..10 {
766 cache.insert(TestKey(format!("key_{i}")), format!("value_{i}"), 100);
767 }
768
769 assert_eq!(cache.len(), 10);
770
771 let removed = cache.evict_under_pressure(50);
773 assert_eq!(removed, 5);
774 assert_eq!(cache.len(), 5);
775 }
776
777 #[test]
778 fn test_pressure_aware_clear_least_used() {
779 let cache: UnifiedCache<TestKey, String> =
780 UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
781
782 for i in 0..10 {
784 cache.insert(TestKey(format!("key_{i}")), format!("value_{i}"), 100);
785 }
786
787 let _ = cache.get(&TestKey("key_0".into()));
789 let _ = cache.get(&TestKey("key_1".into()));
790
791 assert_eq!(cache.len(), 10);
792
793 let removed = cache.clear_least_used(30);
795 assert!(removed <= 4, "Should remove ~3 entries (30% of 10)");
796 assert!(cache.len() >= 6, "Should have ~7 entries left");
797 }
798
799 #[test]
800 fn test_pressure_aware_entries_by_usefulness() {
801 let cache: UnifiedCache<TestKey, String> =
802 UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
803
804 cache.insert(TestKey("hot".into()), "value".to_string(), 100);
806 cache.insert(TestKey("cold".into()), "value".to_string(), 100);
807 cache.insert(TestKey("warm".into()), "value".to_string(), 100);
808
809 for _ in 0..5 {
811 let _ = cache.get(&TestKey("hot".into()));
812 }
813
814 let _ = cache.get(&TestKey("warm".into()));
816
817 let usefulness = cache.entries_by_usefulness();
820 assert_eq!(usefulness.len(), 3);
821
822 assert_eq!(usefulness[0].0.0, "hot");
824 }
825
826 #[test]
827 fn test_pressure_aware_estimate_entry_cost() {
828 let entry = CacheEntry::new("test_value".to_string(), 1000);
829 let cost = UnifiedCache::<TestKey, String>::estimate_entry_cost(&entry);
830
831 assert!(cost >= 1000);
833 assert!(cost <= 1200); }
835}