pub mod fingerprint;
pub mod hierarchy;
pub mod invalidation;
pub mod paging;
pub mod persistent;
pub mod store;
pub mod traits;
pub mod types;
pub use fingerprint::{ContextFingerprintGenerator, Fingerprintable, RollingHasher};
pub use hierarchy::{
CacheTier, HierarchicalCache, HierarchicalCacheConfig, HierarchicalCacheStats, TierConfig,
};
pub use invalidation::{
CacheValidator, DependencyStats, FingerprintInvalidationContext, InvalidationManager,
InvalidationPolicy, InvalidationReason, InvalidationRuleBuilder,
};
pub use paging::{CachePage, PageTable, PagedCache, PagedKVEntry};
pub use persistent::{
CacheIndex, CompactionStats, HybridPersistentCache, IndexEntry, PersistedEntry,
PersistentCacheConfig, PersistentPrefixCache,
};
pub use store::InMemoryPrefixCache;
pub use traits::{PrefixCacheExt, PrefixCacheStore};
pub use types::{
CacheKey, CacheLookupResult, CacheStats, ContextFingerprint, KVCacheEntry, PrefixCacheConfig,
};
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_full_workflow() {
let config = PrefixCacheConfig::new(100, 10 * 1024 * 1024)
.with_default_ttl(600)
.with_compression(false);
let mut cache = InMemoryPrefixCache::new(config);
let generator = ContextFingerprintGenerator::new();
let context = "System: You are a helpful assistant.";
let fingerprint = generator.generate(context);
assert!(!cache.contains(&fingerprint).await);
let kv_data = vec![0.1f32; 256];
let entry = KVCacheEntry::new("system_prompt", fingerprint.clone(), kv_data.clone(), 10);
let key = cache.put(entry).await.unwrap();
assert!(!key.is_empty());
assert!(cache.contains(&fingerprint).await);
let retrieved = cache.get(&fingerprint).await.unwrap();
assert_eq!(retrieved.kv_data.len(), 256);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.entry_count, 1);
}
#[tokio::test]
async fn test_fingerprintable_trait() {
let content = "Test content for fingerprinting";
let fp1 = content.fingerprint();
let fp2 = content.to_string().fingerprint();
assert_eq!(fp1.hash, fp2.hash);
assert_eq!(fp1.prefix_length, fp2.prefix_length);
}
#[tokio::test]
async fn test_rolling_hash_incremental() {
let mut hasher = RollingHasher::new();
hasher.append_str("Hello");
let fp1 = hasher.to_fingerprint("Hello");
hasher.append_str(", world!");
let fp2 = hasher.to_fingerprint("Hello, world!");
assert_ne!(fp1.hash, fp2.hash);
assert!(fp2.prefix_length > fp1.prefix_length);
}
#[tokio::test]
async fn test_prefix_hierarchy_lookup() {
let mut cache = InMemoryPrefixCache::with_defaults();
let generator = ContextFingerprintGenerator::new();
let short_content = "Hello";
let short_fp = generator.generate(short_content);
let short_entry = KVCacheEntry::new("short", short_fp.clone(), vec![1.0; 10], 5);
cache.put(short_entry).await.unwrap();
let long_content = "Hello, world! How are you?";
let long_fp = generator.generate(long_content);
let result = cache.lookup(&long_fp);
match result {
CacheLookupResult::PartialHit {
entry,
remaining_length,
} => {
assert_eq!(entry.fingerprint.prefix_length, short_fp.prefix_length);
assert!(remaining_length > 0);
}
CacheLookupResult::Hit(_) | CacheLookupResult::Miss => {
}
}
}
#[tokio::test]
async fn test_cache_ext_get_or_compute() {
let mut cache = InMemoryPrefixCache::with_defaults();
let fingerprint = ContextFingerprint::new(42, 100, "test");
let entry1 = cache
.get_or_compute(&fingerprint, || {
Ok(KVCacheEntry::new(
"computed",
fingerprint.clone(),
vec![1.0, 2.0, 3.0],
100,
))
})
.await
.unwrap();
assert_eq!(entry1.kv_data, vec![1.0, 2.0, 3.0]);
let entry2 = cache
.get_or_compute(&fingerprint, || {
Ok(KVCacheEntry::new(
"should_not_be_used",
fingerprint.clone(),
vec![4.0, 5.0, 6.0],
100,
))
})
.await
.unwrap();
assert_eq!(entry2.kv_data, vec![1.0, 2.0, 3.0]);
}
#[tokio::test]
#[allow(clippy::cast_precision_loss, clippy::float_cmp)]
async fn test_multiple_entries_different_fingerprints() {
let mut cache = InMemoryPrefixCache::with_defaults();
let generator = ContextFingerprintGenerator::new();
let contexts = [
"Context A: System prompt for task A",
"Context B: System prompt for task B",
"Context C: System prompt for task C",
];
for (i, ctx) in contexts.iter().enumerate() {
let fp = generator.generate(ctx);
let kv_data = vec![i as f32; 10];
let entry = KVCacheEntry::new(format!("ctx_{i}"), fp, kv_data, 100);
cache.put(entry).await.expect("put failed");
}
assert_eq!(cache.len(), 3);
for (i, ctx) in contexts.iter().enumerate() {
let fp = generator.generate(ctx);
let retrieved = cache.get(&fp).await.expect("get failed");
assert!((retrieved.kv_data[0] - i as f32).abs() < f32::EPSILON);
}
}
#[tokio::test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
async fn test_eviction_maintains_consistency() {
let config = PrefixCacheConfig::new(3, 10 * 1024 * 1024);
let mut cache = InMemoryPrefixCache::new(config);
for i in 0..5_i32 {
let fp = ContextFingerprint::new(i as u64, 100, format!("test {i}"));
let kv_data = vec![i as f32; 10];
let entry = KVCacheEntry::new(format!("entry_{i}"), fp, kv_data, 100);
cache.put(entry).await.expect("put failed");
}
assert!(cache.len() <= 3);
let stats = cache.stats();
assert!(stats.evictions >= 2);
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_paged_cache_integration() {
let cache = PagedCache::new(256, 100);
let fp = ContextFingerprint::new(12345, 100, "test");
let data: Vec<f32> = (0..500).map(|i| i as f32).collect();
let _ = cache.put(&fp, &data);
let retrieved = cache.get(&fp).expect("get failed");
assert_eq!(retrieved, data);
}
#[test]
fn test_page_table_operations() {
let mut table = PageTable::new(256, 10);
let id1 = table.allocate_page().unwrap();
let _id2 = table.allocate_page().unwrap();
assert_eq!(table.allocated_count(), 2);
table.free_page(id1);
assert_eq!(table.allocated_count(), 1);
let id3 = table.allocate_page().unwrap();
assert_eq!(id3, id1);
}
#[tokio::test]
async fn test_hierarchical_cache_integration() {
let config = HierarchicalCacheConfig::two_tier(
TierConfig::new(10, 1024 * 1024),
TierConfig::new(50, 5 * 1024 * 1024),
);
let mut cache = HierarchicalCache::new(config);
let fp = ContextFingerprint::new(12345, 100, "test");
let entry = KVCacheEntry::new("test", fp.clone(), vec![1.0; 10], 100);
cache.put(entry).await.unwrap();
assert!(cache.contains(&fp).await);
let stats = cache.hierarchical_stats();
assert_eq!(stats.l1_entries, 1);
}
#[tokio::test]
async fn test_hierarchical_cache_stats_tracking() {
let mut cache = HierarchicalCache::with_defaults();
let fp = ContextFingerprint::new(12345, 100, "test");
let entry = KVCacheEntry::new("test", fp.clone(), vec![1.0; 10], 100);
cache.put(entry).await.unwrap();
cache.get(&fp).await;
let stats = cache.hierarchical_stats();
assert_eq!(stats.l1_hits, 1);
assert_eq!(stats.total_hits, 1);
}
#[test]
fn test_invalidation_manager_integration() {
let policy = InvalidationRuleBuilder::new()
.with_ttl_secs(3600)
.with_max_stale_secs(600)
.build();
let mut manager = InvalidationManager::new(policy);
manager.register_dependency("child1".to_string(), "parent".to_string());
manager.register_dependency("child2".to_string(), "parent".to_string());
let invalidated = manager.invalidate_dependents(&"parent".to_string());
assert!(invalidated.contains(&"child1".to_string()));
assert!(invalidated.contains(&"child2".to_string()));
}
#[test]
fn test_cache_validator_integration() {
let manager = InvalidationManager::new(InvalidationPolicy::ttl_secs(3600));
let validator = CacheValidator::new(manager);
let fp = ContextFingerprint::new(12345, 100, "test");
let entry = KVCacheEntry::new("test", fp, vec![1.0; 10], 100);
assert!(validator.is_valid(&entry));
assert!(validator.validate(&entry).is_ok());
}
#[test]
fn test_fingerprint_invalidation_context_integration() {
let mut ctx = FingerprintInvalidationContext::new();
let fp1 = ContextFingerprint::new(111, 100, "test1");
let fp2 = ContextFingerprint::new(222, 100, "test2");
ctx.register(&fp1, "key1".to_string());
ctx.register(&fp2, "key2".to_string());
assert_eq!(ctx.len(), 2);
assert_eq!(ctx.get_key(&fp1), Some(&"key1".to_string()));
ctx.unregister_by_fingerprint(&fp1);
assert_eq!(ctx.len(), 1);
}
#[tokio::test]
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
async fn test_paged_hierarchical_workflow() {
let paged_cache = PagedCache::new(128, 100);
for i in 0..10_i32 {
let fp = ContextFingerprint::new(i as u64, 100, format!("entry{i}"));
let data: Vec<f32> = (0..200).map(|j| (i * 200 + j) as f32).collect();
let _ = paged_cache.put(&fp, &data);
}
assert_eq!(paged_cache.entry_count(), 10);
let evicted = paged_cache.evict_lru_entries(3);
assert_eq!(evicted, 3);
assert_eq!(paged_cache.entry_count(), 7);
}
#[tokio::test]
async fn test_invalidation_with_cache() {
let mut cache = InMemoryPrefixCache::with_defaults();
let mut manager = InvalidationManager::with_defaults();
let fp1 = ContextFingerprint::new(111, 100, "parent");
let fp2 = ContextFingerprint::new(222, 100, "child");
let entry1 = KVCacheEntry::new("parent_key", fp1.clone(), vec![1.0; 10], 100);
let entry2 = KVCacheEntry::new("child_key", fp2.clone(), vec![2.0; 10], 100);
let key1 = cache.put(entry1).await.unwrap();
let key2 = cache.put(entry2).await.unwrap();
manager.register_dependency(key2.clone(), key1.clone());
let to_invalidate = manager.invalidate_dependents(&key1);
assert!(to_invalidate.contains(&key2));
for key in to_invalidate {
cache.remove(&key).await;
}
cache.remove(&key1).await;
assert_eq!(cache.len(), 0);
}
#[test]
#[allow(clippy::no_effect_underscore_binding)]
fn test_all_exports_accessible() {
let _fp = ContextFingerprint::new(1, 1, "test");
let _config = PrefixCacheConfig::default();
let _stats = CacheStats::default();
let _generator = ContextFingerprintGenerator::new();
let _hasher = RollingHasher::new();
let _tier = TierConfig::default();
let _h_config = HierarchicalCacheConfig::default();
let _h_stats = HierarchicalCacheStats::default();
let _policy = InvalidationPolicy::default();
let reason = InvalidationReason::Manual;
assert!(matches!(reason, InvalidationReason::Manual));
let _manager = InvalidationManager::with_defaults();
let _builder = InvalidationRuleBuilder::new();
let _dep_stats = DependencyStats::default();
let _fp_ctx = FingerprintInvalidationContext::new();
let _page = CachePage::new(1, 256);
let _table = PageTable::new(256, 10);
let _paged = PagedCache::new(256, 10);
let _paged_entry = PagedKVEntry::new(ContextFingerprint::new(1, 1, "test"), vec![1], 100);
}
}