use crate::error::{GraphError, Result};
use crate::graph::Id;
use lru::LruCache;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::hash::Hash;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub struct HierarchicalCache<K, V>
where
K: Hash + Eq + Clone,
V: Clone,
{
hot_cache: Arc<RwLock<LruCache<K, CacheEntry<V>>>>,
cold_cache: Arc<RwLock<LruCache<K, CacheEntry<V>>>>,
stats: Arc<RwLock<CacheStats>>,
config: CacheConfig,
}
#[derive(Debug, Clone)]
struct CacheEntry<V> {
value: V,
access_count: u32,
last_access: Instant,
#[allow(dead_code)]
size_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub hot_cache_size: usize,
pub cold_cache_size: usize,
pub promotion_threshold: u32,
pub cold_cache_ttl: Duration,
pub enable_warming: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
hot_cache_size: 10_000,
cold_cache_size: 100_000,
promotion_threshold: 3,
cold_cache_ttl: Duration::from_secs(300), enable_warming: true,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct CacheStats {
pub hot_hits: u64,
pub cold_hits: u64,
pub misses: u64,
pub promotions: u64,
pub evictions: u64,
pub total_requests: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
if self.total_requests == 0 {
0.0
} else {
(self.hot_hits + self.cold_hits) as f64 / self.total_requests as f64
}
}
pub fn hot_hit_rate(&self) -> f64 {
if self.total_requests == 0 {
0.0
} else {
self.hot_hits as f64 / self.total_requests as f64
}
}
}
impl<K, V> HierarchicalCache<K, V>
where
K: Hash + Eq + Clone,
V: Clone,
{
pub fn new(config: CacheConfig) -> Result<Self> {
let hot_cap = NonZeroUsize::new(config.hot_cache_size)
.ok_or_else(|| GraphError::Memory("Invalid hot cache size".to_string()))?;
let cold_cap = NonZeroUsize::new(config.cold_cache_size)
.ok_or_else(|| GraphError::Memory("Invalid cold cache size".to_string()))?;
Ok(Self {
hot_cache: Arc::new(RwLock::new(LruCache::new(hot_cap))),
cold_cache: Arc::new(RwLock::new(LruCache::new(cold_cap))),
stats: Arc::new(RwLock::new(CacheStats::default())),
config,
})
}
pub fn get(&self, key: &K) -> Option<V> {
let mut stats = self.stats.write();
stats.total_requests += 1;
{
let mut hot_cache = self.hot_cache.write();
if let Some(entry) = hot_cache.get_mut(key) {
entry.access_count += 1;
entry.last_access = Instant::now();
stats.hot_hits += 1;
return Some(entry.value.clone());
}
}
{
let mut cold_cache = self.cold_cache.write();
if let Some(entry) = cold_cache.get_mut(key) {
entry.access_count += 1;
entry.last_access = Instant::now();
stats.cold_hits += 1;
let value = entry.value.clone();
if entry.access_count >= self.config.promotion_threshold {
let promoted_entry = entry.clone();
cold_cache.pop(key); drop(cold_cache);
let mut hot_cache = self.hot_cache.write();
if hot_cache.len() >= hot_cache.cap().get() {
hot_cache.pop_lru();
stats.evictions += 1;
}
hot_cache.put(key.clone(), promoted_entry);
stats.promotions += 1;
}
return Some(value);
}
}
stats.misses += 1;
None
}
pub fn put(&self, key: K, value: V, size_bytes: usize) {
let entry = CacheEntry {
value,
access_count: 1,
last_access: Instant::now(),
size_bytes,
};
let mut cold_cache = self.cold_cache.write();
if cold_cache.len() >= cold_cache.cap().get() {
cold_cache.pop_lru();
let mut stats = self.stats.write();
stats.evictions += 1;
}
cold_cache.put(key, entry);
}
pub fn put_hot(&self, key: K, value: V, size_bytes: usize) {
let entry = CacheEntry {
value,
access_count: 10, last_access: Instant::now(),
size_bytes,
};
let mut hot_cache = self.hot_cache.write();
if hot_cache.len() >= hot_cache.cap().get() {
hot_cache.pop_lru();
let mut stats = self.stats.write();
stats.evictions += 1;
}
hot_cache.put(key, entry);
}
pub fn cleanup_expired(&self) {
let now = Instant::now();
let ttl = self.config.cold_cache_ttl;
let mut cold_cache = self.cold_cache.write();
let mut to_remove = Vec::new();
for (key, entry) in cold_cache.iter() {
if now.duration_since(entry.last_access) > ttl {
to_remove.push(key.clone());
}
}
for key in to_remove {
cold_cache.pop(&key);
let mut stats = self.stats.write();
stats.evictions += 1;
}
}
pub fn stats(&self) -> CacheStats {
self.stats.read().clone()
}
pub fn clear(&self) {
self.hot_cache.write().clear();
self.cold_cache.write().clear();
*self.stats.write() = CacheStats::default();
}
pub fn sizes(&self) -> (usize, usize) {
(self.hot_cache.read().len(), self.cold_cache.read().len())
}
}
pub struct NodeCache {
cache: HierarchicalCache<Id, Vec<u8>>,
property_cache: Arc<RwLock<LruCache<(Id, String), serde_json::Value>>>,
access_patterns: Arc<RwLock<HashMap<Id, AccessPattern>>>,
}
#[derive(Debug, Clone)]
struct AccessPattern {
access_count: u32,
last_access: Instant,
frequently_accessed_properties: Vec<String>,
}
impl NodeCache {
pub fn new(config: CacheConfig) -> Result<Self> {
let property_cap = NonZeroUsize::new(config.hot_cache_size * 5)
.ok_or_else(|| GraphError::Memory("Invalid property cache size".to_string()))?;
Ok(Self {
cache: HierarchicalCache::new(config)?,
property_cache: Arc::new(RwLock::new(LruCache::new(property_cap))),
access_patterns: Arc::new(RwLock::new(HashMap::new())),
})
}
pub fn get_node(&self, id: Id) -> Option<Vec<u8>> {
self.update_access_pattern(id);
self.cache.get(&id)
}
pub fn put_node(&self, id: Id, data: Vec<u8>) {
let size = data.len();
self.cache.put(id, data, size);
}
pub fn get_property(&self, node_id: Id, property: &str) -> Option<serde_json::Value> {
let key = (node_id, property.to_string());
self.property_cache.write().get(&key).cloned()
}
pub fn put_property(&self, node_id: Id, property: String, value: serde_json::Value) {
let key = (node_id, property.clone());
let mut cache = self.property_cache.write();
cache.put(key, value);
let mut patterns = self.access_patterns.write();
if let Some(pattern) = patterns.get_mut(&node_id) {
if !pattern.frequently_accessed_properties.contains(&property) {
pattern.frequently_accessed_properties.push(property);
}
}
}
fn update_access_pattern(&self, node_id: Id) {
let mut patterns = self.access_patterns.write();
let pattern = patterns.entry(node_id).or_insert_with(|| AccessPattern {
access_count: 0,
last_access: Instant::now(),
frequently_accessed_properties: Vec::new(),
});
pattern.access_count += 1;
pattern.last_access = Instant::now();
}
pub fn warm_cache_for_traversal(&self, starting_nodes: &[Id], _related_nodes: &[Id]) {
for &node_id in starting_nodes {
self.update_access_pattern(node_id);
}
}
pub fn stats(&self) -> (CacheStats, usize) {
let cache_stats = self.cache.stats();
let property_cache_size = self.property_cache.read().len();
(cache_stats, property_cache_size)
}
}
pub struct RelationshipCache {
cache: HierarchicalCache<Id, Vec<u8>>,
adjacency_cache: Arc<RwLock<LruCache<Id, Vec<Id>>>>,
}
impl RelationshipCache {
pub fn new(config: CacheConfig) -> Result<Self> {
let adj_cap = NonZeroUsize::new(config.hot_cache_size)
.ok_or_else(|| GraphError::Memory("Invalid adjacency cache size".to_string()))?;
Ok(Self {
cache: HierarchicalCache::new(config)?,
adjacency_cache: Arc::new(RwLock::new(LruCache::new(adj_cap))),
})
}
pub fn get_relationship(&self, id: Id) -> Option<Vec<u8>> {
self.cache.get(&id)
}
pub fn put_relationship(&self, id: Id, data: Vec<u8>) {
let size = data.len();
self.cache.put(id, data, size);
}
pub fn get_adjacency(&self, node_id: Id) -> Option<Vec<Id>> {
self.adjacency_cache.write().get(&node_id).cloned()
}
pub fn put_adjacency(&self, node_id: Id, relationship_ids: Vec<Id>) {
self.adjacency_cache.write().put(node_id, relationship_ids);
}
pub fn stats(&self) -> (CacheStats, usize) {
let cache_stats = self.cache.stats();
let adjacency_cache_size = self.adjacency_cache.read().len();
(cache_stats, adjacency_cache_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hierarchical_cache() {
let config = CacheConfig {
hot_cache_size: 2,
cold_cache_size: 5,
promotion_threshold: 2,
..Default::default()
};
let cache: HierarchicalCache<String, i32> = HierarchicalCache::new(config).unwrap();
cache.put("key1".to_string(), 1, 4);
cache.put("key2".to_string(), 2, 4);
assert_eq!(cache.get(&"key1".to_string()), Some(1));
assert_eq!(cache.get(&"key1".to_string()), Some(1));
assert_eq!(cache.get(&"key1".to_string()), Some(1));
let stats = cache.stats();
assert!(stats.hit_rate() > 0.0);
assert!(stats.promotions > 0);
}
#[test]
fn test_node_cache() {
let config = CacheConfig::default();
let cache = NodeCache::new(config).unwrap();
let node_id = 1;
let node_data = vec![1, 2, 3, 4];
cache.put_node(node_id, node_data.clone());
assert_eq!(cache.get_node(node_id), Some(node_data));
cache.put_property(node_id, "name".to_string(), serde_json::json!("test"));
assert_eq!(
cache.get_property(node_id, "name"),
Some(serde_json::json!("test"))
);
}
#[test]
fn test_relationship_cache() {
let config = CacheConfig::default();
let cache = RelationshipCache::new(config).unwrap();
let rel_id = 1;
let rel_data = vec![5, 6, 7, 8];
let node_id = 10;
let adjacency = vec![1, 2, 3];
cache.put_relationship(rel_id, rel_data.clone());
cache.put_adjacency(node_id, adjacency.clone());
assert_eq!(cache.get_relationship(rel_id), Some(rel_data));
assert_eq!(cache.get_adjacency(node_id), Some(adjacency));
}
}