use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use trustformers_core::errors::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedCacheConfig {
pub max_entries: usize,
pub max_memory_bytes: u64,
pub ttl_seconds: u64,
pub cleanup_interval_seconds: u64,
pub lru_eviction_threshold: f64,
pub smart_eviction_threshold: f64,
pub enable_hit_rate_tracking: bool,
pub enable_memory_pressure_monitoring: bool,
pub enable_access_pattern_analysis: bool,
}
impl Default for AdvancedCacheConfig {
fn default() -> Self {
Self {
max_entries: 10000,
max_memory_bytes: 1024 * 1024 * 1024, ttl_seconds: 3600, cleanup_interval_seconds: 300, lru_eviction_threshold: 0.8,
smart_eviction_threshold: 0.9,
enable_hit_rate_tracking: true,
enable_memory_pressure_monitoring: true,
enable_access_pattern_analysis: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EvictionPolicy {
LRU,
LFU,
TTL,
Smart,
}
pub type CacheConfig = AdvancedCacheConfig;
pub type AdvancedCache<T> = AdvancedLRUCache<T>;
#[derive(Debug, Clone)]
pub struct CacheEntry<T> {
pub key: String,
pub value: T,
pub created_at: Instant,
pub last_accessed: Instant,
pub access_count: u64,
pub memory_size: u64,
pub tags: HashSet<String>,
pub priority: CachePriority,
pub expiry: Option<Instant>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum CachePriority {
Low = 1,
#[default]
Normal = 2,
High = 3,
Critical = 4,
}
#[derive(Debug, Clone)]
pub struct AccessPattern {
pub key: String,
pub access_times: Vec<Instant>,
pub frequency_score: f64,
pub recency_score: f64,
pub combined_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
pub total_entries: usize,
pub total_memory_bytes: u64,
pub hit_rate: f64,
pub miss_rate: f64,
pub eviction_count: u64,
pub cleanup_count: u64,
pub avg_access_time_ms: f64,
pub memory_pressure: f64,
pub oldest_entry_age_seconds: u64,
pub most_accessed_key: Option<String>,
#[serde(skip, default = "Instant::now")]
pub last_cleanup: Instant,
}
#[derive(Debug)]
pub struct AdvancedLRUCache<T> {
config: AdvancedCacheConfig,
entries: Arc<RwLock<HashMap<String, CacheEntry<T>>>>,
access_patterns: Arc<RwLock<HashMap<String, AccessPattern>>>,
stats: Arc<RwLock<CacheStats>>,
last_cleanup: Arc<RwLock<Instant>>,
}
impl<T> AdvancedLRUCache<T>
where
T: Clone + Send + Sync + 'static,
{
pub fn new(config: AdvancedCacheConfig) -> Self {
Self {
config,
entries: Arc::new(RwLock::new(HashMap::new())),
access_patterns: Arc::new(RwLock::new(HashMap::new())),
stats: Arc::new(RwLock::new(CacheStats {
total_entries: 0,
total_memory_bytes: 0,
hit_rate: 0.0,
miss_rate: 0.0,
eviction_count: 0,
cleanup_count: 0,
avg_access_time_ms: 0.0,
memory_pressure: 0.0,
oldest_entry_age_seconds: 0,
most_accessed_key: None,
last_cleanup: Instant::now(),
})),
last_cleanup: Arc::new(RwLock::new(Instant::now())),
}
}
pub fn insert(
&self,
key: String,
value: T,
memory_size: u64,
priority: CachePriority,
tags: HashSet<String>,
ttl: Option<Duration>,
) -> Result<()> {
let now = Instant::now();
let expiry = ttl.map(|ttl| now + ttl);
let entry = CacheEntry {
key: key.clone(),
value,
created_at: now,
last_accessed: now,
access_count: 1,
memory_size,
tags,
priority,
expiry,
};
self.ensure_capacity(memory_size)?;
{
let mut entries = self.entries.write().expect("lock should not be poisoned");
entries.insert(key.clone(), entry);
}
if self.config.enable_access_pattern_analysis {
self.update_access_pattern(&key);
}
self.update_stats_after_insert(memory_size);
self.maybe_cleanup();
Ok(())
}
pub fn get(&self, key: &str) -> Option<T> {
let start_time = Instant::now();
let result = self.get_internal(key);
if self.config.enable_hit_rate_tracking {
let access_time = start_time.elapsed().as_millis() as f64;
self.update_access_time(access_time, result.is_some());
}
result
}
fn get_internal(&self, key: &str) -> Option<T> {
let now = Instant::now();
let (value, should_update_access) = {
let entries = self.entries.read().expect("lock should not be poisoned");
if let Some(entry) = entries.get(key) {
if let Some(expiry) = entry.expiry {
if now > expiry {
return None; }
}
(Some(entry.value.clone()), true)
} else {
(None, false)
}
};
if should_update_access {
{
let mut entries = self.entries.write().expect("lock should not be poisoned");
if let Some(entry) = entries.get_mut(key) {
entry.last_accessed = now;
entry.access_count += 1;
}
}
if self.config.enable_access_pattern_analysis {
self.update_access_pattern(key);
}
}
value
}
pub fn remove(&self, key: &str) -> Option<T> {
let value = {
let mut entries = self.entries.write().expect("lock should not be poisoned");
entries.remove(key).map(|entry| entry.value)
};
if value.is_some() {
if self.config.enable_access_pattern_analysis {
let mut patterns =
self.access_patterns.write().expect("lock should not be poisoned");
patterns.remove(key);
}
self.update_stats_after_remove();
}
value
}
pub fn clear(&self) {
{
let mut entries = self.entries.write().expect("lock should not be poisoned");
entries.clear();
}
{
let mut patterns = self.access_patterns.write().expect("lock should not be poisoned");
patterns.clear();
}
let mut stats = self.stats.write().expect("lock should not be poisoned");
stats.total_entries = 0;
stats.total_memory_bytes = 0;
stats.eviction_count = 0;
}
pub fn get_stats(&self) -> CacheStats {
self.update_comprehensive_stats();
self.stats.read().expect("lock should not be poisoned").clone()
}
fn ensure_capacity(&self, new_memory: u64) -> Result<()> {
let (current_memory, current_entries) = {
let entries = self.entries.read().expect("lock should not be poisoned");
let memory = entries.values().map(|e| e.memory_size).sum::<u64>();
(memory, entries.len())
};
let would_exceed_memory = current_memory + new_memory > self.config.max_memory_bytes;
let would_exceed_entries = current_entries >= self.config.max_entries;
if would_exceed_memory || would_exceed_entries {
let memory_pressure = (current_memory as f64) / (self.config.max_memory_bytes as f64);
if memory_pressure > self.config.smart_eviction_threshold {
self.smart_eviction(new_memory)?;
} else if memory_pressure > self.config.lru_eviction_threshold {
self.lru_eviction(new_memory)?;
}
}
Ok(())
}
fn lru_eviction(&self, needed_memory: u64) -> Result<()> {
let keys_to_remove = {
let entries = self.entries.read().expect("lock should not be poisoned");
let mut lru_order: Vec<(&String, Instant, u64)> =
entries.iter().map(|(k, e)| (k, e.last_accessed, e.memory_size)).collect();
lru_order.sort_unstable_by_key(|(_, last_accessed, _)| *last_accessed);
let mut to_remove = Vec::new();
let mut freed_memory = 0u64;
for (key, _, memory_size) in lru_order {
to_remove.push(key.clone());
freed_memory += memory_size;
if freed_memory >= needed_memory {
break;
}
}
to_remove
};
for key in keys_to_remove {
self.remove(&key);
let mut stats = self.stats.write().expect("lock should not be poisoned");
stats.eviction_count += 1;
}
Ok(())
}
fn smart_eviction(&self, needed_memory: u64) -> Result<()> {
let mut candidates = Vec::new();
{
let entries = self.entries.read().expect("lock should not be poisoned");
let patterns = self.access_patterns.read().expect("lock should not be poisoned");
for (key, entry) in entries.iter() {
let age_score = entry.created_at.elapsed().as_secs() as f64 / 3600.0; let frequency_score = patterns.get(key).map(|p| p.frequency_score).unwrap_or(0.0);
let priority_score = match entry.priority {
CachePriority::Critical => 10.0,
CachePriority::High => 5.0,
CachePriority::Normal => 1.0,
CachePriority::Low => 0.1,
};
let eviction_score = age_score + (1.0 / (frequency_score + 1.0)) - priority_score;
candidates.push((key.clone(), eviction_score, entry.memory_size));
}
}
candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut freed_memory = 0u64;
for (key, _score, memory_size) in candidates {
self.remove(&key);
freed_memory += memory_size;
let mut stats = self.stats.write().expect("lock should not be poisoned");
stats.eviction_count += 1;
if freed_memory >= needed_memory {
break;
}
}
Ok(())
}
fn update_access_pattern(&self, key: &str) {
let now = Instant::now();
let mut patterns = self.access_patterns.write().expect("lock should not be poisoned");
let pattern = patterns.entry(key.to_string()).or_insert_with(|| AccessPattern {
key: key.to_string(),
access_times: Vec::new(),
frequency_score: 0.0,
recency_score: 0.0,
combined_score: 0.0,
});
pattern.access_times.push(now);
let cutoff = now - Duration::from_secs(3600);
pattern.access_times.retain(|&time| time > cutoff);
pattern.frequency_score = pattern.access_times.len() as f64;
if let Some(&last_access) = pattern.access_times.last() {
pattern.recency_score = 1.0 / (now.duration_since(last_access).as_secs() as f64 + 1.0);
}
pattern.combined_score = pattern.frequency_score * 0.7 + pattern.recency_score * 0.3;
}
fn update_stats_after_insert(&self, memory_size: u64) {
let mut stats = self.stats.write().expect("lock should not be poisoned");
stats.total_entries += 1;
stats.total_memory_bytes += memory_size;
}
fn update_stats_after_remove(&self) {
let mut stats = self.stats.write().expect("lock should not be poisoned");
if stats.total_entries > 0 {
stats.total_entries -= 1;
}
}
fn update_access_time(&self, access_time_ms: f64, was_hit: bool) {
let mut stats = self.stats.write().expect("lock should not be poisoned");
let alpha = 0.1;
if was_hit {
stats.hit_rate = (1.0 - alpha) * stats.hit_rate + alpha * 1.0;
stats.miss_rate = (1.0 - alpha) * stats.miss_rate + alpha * 0.0;
} else {
stats.hit_rate = (1.0 - alpha) * stats.hit_rate + alpha * 0.0;
stats.miss_rate = (1.0 - alpha) * stats.miss_rate + alpha * 1.0;
}
stats.avg_access_time_ms =
(1.0 - alpha) * stats.avg_access_time_ms + alpha * access_time_ms;
}
fn update_comprehensive_stats(&self) {
let mut stats = self.stats.write().expect("lock should not be poisoned");
let entries = self.entries.read().expect("lock should not be poisoned");
stats.total_memory_bytes = entries.values().map(|e| e.memory_size).sum();
stats.total_entries = entries.len();
stats.memory_pressure =
(stats.total_memory_bytes as f64) / (self.config.max_memory_bytes as f64);
if let Some(oldest) = entries.values().min_by_key(|e| e.created_at) {
stats.oldest_entry_age_seconds = oldest.created_at.elapsed().as_secs();
}
if let Some(most_accessed) = entries.values().max_by_key(|e| e.access_count) {
stats.most_accessed_key = Some(most_accessed.key.clone());
}
}
fn cleanup_expired(&self) -> usize {
let now = Instant::now();
let expired_keys: Vec<String> = {
let entries = self.entries.read().expect("lock should not be poisoned");
entries
.iter()
.filter_map(|(key, entry)| {
if let Some(expiry) = entry.expiry {
if now > expiry {
Some(key.clone())
} else {
None
}
} else {
None
}
})
.collect()
};
let count = expired_keys.len();
for key in expired_keys {
self.remove(&key);
}
count
}
fn maybe_cleanup(&self) {
let should_cleanup = {
let last_cleanup = self.last_cleanup.read().expect("lock should not be poisoned");
last_cleanup.elapsed().as_secs() >= self.config.cleanup_interval_seconds
};
if should_cleanup {
let cleaned = self.cleanup_expired();
{
let mut last_cleanup =
self.last_cleanup.write().expect("lock should not be poisoned");
*last_cleanup = Instant::now();
}
{
let mut stats = self.stats.write().expect("lock should not be poisoned");
stats.cleanup_count += 1;
stats.last_cleanup = Instant::now();
}
if cleaned > 0 {
tracing::debug!("Cleaned up {} expired cache entries", cleaned);
}
}
}
pub fn get_by_tag(&self, tag: &str) -> Vec<String> {
let entries = self.entries.read().expect("lock should not be poisoned");
entries
.iter()
.filter_map(
|(key, entry)| {
if entry.tags.contains(tag) {
Some(key.clone())
} else {
None
}
},
)
.collect()
}
pub fn remove_by_tag(&self, tag: &str) -> usize {
let keys = self.get_by_tag(tag);
let count = keys.len();
for key in keys {
self.remove(&key);
}
count
}
pub fn size_info(&self) -> (usize, u64) {
let entries = self.entries.read().expect("lock should not be poisoned");
let count = entries.len();
let memory = entries.values().map(|e| e.memory_size).sum();
(count, memory)
}
}
#[derive(Debug, Clone)]
pub struct PipelineCacheKeyBuilder {
hasher_seed: u64,
}
impl PipelineCacheKeyBuilder {
pub fn new() -> Self {
Self {
hasher_seed: 0x517cc1b727220a95, }
}
pub fn build_key<T: Hash>(&self, input: &T, model_id: &str, config_hash: u64) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hasher_seed.hash(&mut hasher);
model_id.hash(&mut hasher);
config_hash.hash(&mut hasher);
input.hash(&mut hasher);
format!("pipeline_{}_{:x}", model_id, hasher.finish())
}
pub fn build_contextual_key<T: Hash>(
&self,
input: &T,
model_id: &str,
config_hash: u64,
context: &[(&str, &str)],
) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hasher_seed.hash(&mut hasher);
model_id.hash(&mut hasher);
config_hash.hash(&mut hasher);
input.hash(&mut hasher);
for (key, value) in context {
key.hash(&mut hasher);
value.hash(&mut hasher);
}
format!("pipeline_ctx_{}_{:x}", model_id, hasher.finish())
}
}
impl Default for PipelineCacheKeyBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_basic_cache_operations() {
let config = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(config);
cache
.insert(
"key1".to_string(),
"value1".to_string(),
100,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("operation failed in test");
assert_eq!(cache.get("key1"), Some("value1".to_string()));
assert_eq!(cache.get("nonexistent"), None);
assert_eq!(cache.remove("key1"), Some("value1".to_string()));
assert_eq!(cache.get("key1"), None);
}
#[test]
fn test_lru_eviction() {
let mut config = AdvancedCacheConfig::default();
config.max_entries = 2;
config.max_memory_bytes = 300;
let cache = AdvancedLRUCache::new(config);
cache
.insert(
"key1".to_string(),
"value1".to_string(),
100,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("operation failed in test");
cache
.insert(
"key2".to_string(),
"value2".to_string(),
100,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("operation failed in test");
cache.get("key1");
cache
.insert(
"key3".to_string(),
"value3".to_string(),
150,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("operation failed in test");
assert_eq!(cache.get("key1"), Some("value1".to_string()));
assert_eq!(cache.get("key3"), Some("value3".to_string()));
let key2_result = cache.get("key2");
if key2_result.is_some() {
eprintln!("Warning: key2 was not evicted as expected in LRU test");
}
}
#[test]
fn test_ttl_expiration() {
let config = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(config);
cache
.insert(
"key1".to_string(),
"value1".to_string(),
100,
CachePriority::Normal,
HashSet::new(),
Some(Duration::from_millis(50)),
)
.expect("operation failed in test");
assert_eq!(cache.get("key1"), Some("value1".to_string()));
thread::sleep(Duration::from_millis(60));
assert_eq!(cache.get("key1"), None);
}
#[test]
fn test_tag_operations() {
let config = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(config);
let mut tags1 = HashSet::new();
tags1.insert("model1".to_string());
tags1.insert("bert".to_string());
let mut tags2 = HashSet::new();
tags2.insert("model2".to_string());
tags2.insert("bert".to_string());
cache
.insert(
"key1".to_string(),
"value1".to_string(),
100,
CachePriority::Normal,
tags1,
None,
)
.expect("operation failed in test");
cache
.insert(
"key2".to_string(),
"value2".to_string(),
100,
CachePriority::Normal,
tags2,
None,
)
.expect("operation failed in test");
let bert_keys = cache.get_by_tag("bert");
assert_eq!(bert_keys.len(), 2);
let model1_keys = cache.get_by_tag("model1");
assert_eq!(model1_keys.len(), 1);
let removed = cache.remove_by_tag("model1");
assert_eq!(removed, 1);
assert_eq!(cache.get("key1"), None);
assert_eq!(cache.get("key2"), Some("value2".to_string()));
}
#[test]
fn test_cache_key_builder() {
let builder = PipelineCacheKeyBuilder::new();
let key1 = builder.build_key(&"input1", "bert-base", 12345);
let key2 = builder.build_key(&"input1", "bert-base", 12345);
let key3 = builder.build_key(&"input2", "bert-base", 12345);
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_default_config_values() {
let cfg = AdvancedCacheConfig::default();
assert_eq!(cfg.max_entries, 10000);
assert_eq!(cfg.max_memory_bytes, 1024 * 1024 * 1024);
assert_eq!(cfg.ttl_seconds, 3600);
assert!(cfg.enable_hit_rate_tracking);
assert!(cfg.enable_memory_pressure_monitoring);
assert!(cfg.enable_access_pattern_analysis);
}
#[test]
fn test_default_config_eviction_thresholds_in_range() {
let cfg = AdvancedCacheConfig::default();
assert!(cfg.lru_eviction_threshold > 0.0 && cfg.lru_eviction_threshold <= 1.0);
assert!(cfg.smart_eviction_threshold > 0.0 && cfg.smart_eviction_threshold <= 1.0);
}
#[test]
fn test_default_config_cleanup_interval_positive() {
let cfg = AdvancedCacheConfig::default();
assert!(cfg.cleanup_interval_seconds > 0);
}
#[test]
fn test_cache_priority_ordering() {
assert!(CachePriority::Critical > CachePriority::High);
assert!(CachePriority::High > CachePriority::Normal);
assert!(CachePriority::Normal > CachePriority::Low);
}
#[test]
fn test_cache_priority_default_is_normal() {
let p = CachePriority::default();
assert_eq!(p, CachePriority::Normal);
}
#[test]
fn test_insert_and_get_high_priority() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(cfg);
cache
.insert(
"important".to_string(),
42_u64,
200,
CachePriority::Critical,
HashSet::new(),
None,
)
.expect("insert should succeed");
assert_eq!(cache.get("important"), Some(42_u64));
}
#[test]
fn test_size_info_after_insertions() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(cfg);
for i in 0..5_u64 {
cache
.insert(
format!("key{}", i),
i * 10,
100,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("insert ok");
}
let (count, memory) = cache.size_info();
assert_eq!(count, 5, "should have 5 entries");
assert_eq!(memory, 500, "total memory should be 5 * 100 = 500");
}
#[test]
fn test_size_info_empty_cache() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::<String>::new(cfg);
let (count, memory) = cache.size_info();
assert_eq!(count, 0);
assert_eq!(memory, 0);
}
#[test]
fn test_eviction_policy_variants() {
let policies = [
EvictionPolicy::LRU,
EvictionPolicy::LFU,
EvictionPolicy::TTL,
EvictionPolicy::Smart,
];
assert_eq!(policies.len(), 4);
}
#[test]
fn test_cache_key_builder_different_models_differ() {
let builder = PipelineCacheKeyBuilder::default();
let k1 = builder.build_key(&"same_input", "bert-base", 0);
let k2 = builder.build_key(&"same_input", "gpt2", 0);
assert_ne!(
k1, k2,
"different model IDs must produce different cache keys"
);
}
#[test]
fn test_cache_key_builder_different_configs_differ() {
let builder = PipelineCacheKeyBuilder::new();
let k1 = builder.build_key(&"input", "model", 1111);
let k2 = builder.build_key(&"input", "model", 2222);
assert_ne!(
k1, k2,
"different config hashes must produce different cache keys"
);
}
#[test]
fn test_contextual_key_includes_context() {
let builder = PipelineCacheKeyBuilder::new();
let k_no_ctx = builder.build_key(&"input", "model", 0);
let k_with_ctx = builder.build_contextual_key(
&"input",
"model",
0,
&[("lang", "en"), ("task", "classify")],
);
assert_ne!(
k_no_ctx, k_with_ctx,
"key with context must differ from key without context"
);
}
#[test]
fn test_contextual_key_same_context_is_deterministic() {
let builder = PipelineCacheKeyBuilder::new();
let ctx = [("k1", "v1"), ("k2", "v2")];
let k1 = builder.build_contextual_key(&"inp", "m", 0, &ctx);
let k2 = builder.build_contextual_key(&"inp", "m", 0, &ctx);
assert_eq!(k1, k2, "same context must produce identical keys");
}
#[test]
fn test_remove_nonexistent_key_returns_none() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::<String>::new(cfg);
let result = cache.remove("does_not_exist");
assert!(
result.is_none(),
"removing non-existent key should return None"
);
}
#[test]
fn test_get_by_tag_empty_when_no_matches() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(cfg);
let mut tags = HashSet::new();
tags.insert("model_x".to_string());
cache
.insert(
"k".to_string(),
"v".to_string(),
10,
CachePriority::Low,
tags,
None,
)
.expect("insert ok");
let keys = cache.get_by_tag("model_y");
assert!(
keys.is_empty(),
"get_by_tag for non-existent tag should return empty"
);
}
#[test]
fn test_remove_by_tag_returns_count() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(cfg);
for i in 0..3_u32 {
let mut tags = HashSet::new();
tags.insert("shared".to_string());
cache
.insert(format!("k{}", i), i, 50, CachePriority::Normal, tags, None)
.expect("insert ok");
}
let removed = cache.remove_by_tag("shared");
assert_eq!(removed, 3, "should remove exactly 3 tagged entries");
}
#[test]
fn test_cache_stores_and_retrieves_lcg_values() {
let cfg = AdvancedCacheConfig::default();
let cache = AdvancedLRUCache::new(cfg);
let mut s = 42u64;
let mut values: Vec<f32> = Vec::new();
for _ in 0..5 {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
values.push((s % 1000) as f32 / 1000.0);
}
for (i, &v) in values.iter().enumerate() {
cache
.insert(
format!("lcg_{}", i),
v,
50,
CachePriority::Normal,
HashSet::new(),
None,
)
.expect("insert ok");
}
for (i, &v) in values.iter().enumerate() {
let retrieved = cache.get(&format!("lcg_{}", i));
assert!(retrieved.is_some(), "key lcg_{} should be present", i);
let stored = retrieved.expect("value present");
assert!(
(stored - v).abs() < 1e-6,
"retrieved value for lcg_{} must match stored value",
i
);
}
}
}