rust_logic_graph/cache/
entry.rs

1//! Cache entry and key types
2
3use serde::{Deserialize, Serialize};
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6use std::time::{Duration, SystemTime};
7
8/// Key for cache entries, combining node ID and input hash
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct CacheKey {
11    pub node_id: String,
12    pub input_hash: u64,
13}
14
15impl CacheKey {
16    /// Create a new cache key from node ID and inputs
17    pub fn new(node_id: impl Into<String>, inputs: &serde_json::Value) -> Self {
18        let node_id = node_id.into();
19        let input_hash = Self::hash_inputs(inputs);
20        Self {
21            node_id,
22            input_hash,
23        }
24    }
25
26    /// Hash the inputs to create a stable key
27    fn hash_inputs(inputs: &serde_json::Value) -> u64 {
28        let mut hasher = DefaultHasher::new();
29        // Convert to canonical JSON string for consistent hashing
30        if let Ok(json_str) = serde_json::to_string(inputs) {
31            json_str.hash(&mut hasher);
32        }
33        hasher.finish()
34    }
35}
36
37/// A cached entry with metadata
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CacheEntry {
40    pub key: CacheKey,
41    pub value: serde_json::Value,
42    pub created_at: SystemTime,
43    pub last_accessed: SystemTime,
44    pub access_count: u64,
45    pub ttl: Option<Duration>,
46    pub size_bytes: usize,
47}
48
49impl CacheEntry {
50    /// Create a new cache entry
51    pub fn new(
52        key: CacheKey,
53        value: serde_json::Value,
54        ttl: Option<Duration>,
55    ) -> Self {
56        let size_bytes = Self::estimate_size(&value);
57        let now = SystemTime::now();
58
59        Self {
60            key,
61            value,
62            created_at: now,
63            last_accessed: now,
64            access_count: 0,
65            ttl,
66            size_bytes,
67        }
68    }
69
70    /// Check if the entry has expired
71    pub fn is_expired(&self) -> bool {
72        if let Some(ttl) = self.ttl {
73            if let Ok(elapsed) = self.created_at.elapsed() {
74                return elapsed > ttl;
75            }
76        }
77        false
78    }
79
80    /// Update access metadata
81    pub fn mark_accessed(&mut self) {
82        self.last_accessed = SystemTime::now();
83        self.access_count += 1;
84    }
85
86    /// Estimate the memory size of the cached value
87    fn estimate_size(value: &serde_json::Value) -> usize {
88        // Rough estimation based on JSON serialization
89        match serde_json::to_string(value) {
90            Ok(s) => s.len(),
91            Err(_) => 0,
92        }
93    }
94
95    /// Get age of the entry
96    pub fn age(&self) -> Duration {
97        self.created_at.elapsed().unwrap_or(Duration::ZERO)
98    }
99
100    /// Get time since last access
101    pub fn idle_time(&self) -> Duration {
102        self.last_accessed.elapsed().unwrap_or(Duration::ZERO)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use serde_json::json;
110
111    #[test]
112    fn test_cache_key_creation() {
113        let inputs = json!({"x": 10, "y": 20});
114        let key1 = CacheKey::new("node1", &inputs);
115        let key2 = CacheKey::new("node1", &inputs);
116        
117        assert_eq!(key1, key2);
118        assert_eq!(key1.node_id, "node1");
119    }
120
121    #[test]
122    fn test_cache_key_different_inputs() {
123        let inputs1 = json!({"x": 10});
124        let inputs2 = json!({"x": 20});
125        
126        let key1 = CacheKey::new("node1", &inputs1);
127        let key2 = CacheKey::new("node1", &inputs2);
128        
129        assert_ne!(key1.input_hash, key2.input_hash);
130    }
131
132    #[test]
133    fn test_cache_entry_expiration() {
134        let key = CacheKey::new("node1", &json!({}));
135        let mut entry = CacheEntry::new(
136            key,
137            json!({"result": 42}),
138            Some(Duration::from_millis(1)),
139        );
140
141        assert!(!entry.is_expired());
142        
143        std::thread::sleep(Duration::from_millis(10));
144        assert!(entry.is_expired());
145    }
146
147    #[test]
148    fn test_cache_entry_no_expiration() {
149        let key = CacheKey::new("node1", &json!({}));
150        let entry = CacheEntry::new(key, json!({"result": 42}), None);
151        
152        assert!(!entry.is_expired());
153    }
154
155    #[test]
156    fn test_cache_entry_access_tracking() {
157        let key = CacheKey::new("node1", &json!({}));
158        let mut entry = CacheEntry::new(key, json!({"result": 42}), None);
159        
160        assert_eq!(entry.access_count, 0);
161        
162        entry.mark_accessed();
163        assert_eq!(entry.access_count, 1);
164        
165        entry.mark_accessed();
166        assert_eq!(entry.access_count, 2);
167    }
168}