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(key: CacheKey, value: serde_json::Value, ttl: Option<Duration>) -> Self {
52        let size_bytes = Self::estimate_size(&value);
53        let now = SystemTime::now();
54
55        Self {
56            key,
57            value,
58            created_at: now,
59            last_accessed: now,
60            access_count: 0,
61            ttl,
62            size_bytes,
63        }
64    }
65
66    /// Check if the entry has expired
67    pub fn is_expired(&self) -> bool {
68        if let Some(ttl) = self.ttl {
69            if let Ok(elapsed) = self.created_at.elapsed() {
70                return elapsed > ttl;
71            }
72        }
73        false
74    }
75
76    /// Update access metadata
77    pub fn mark_accessed(&mut self) {
78        self.last_accessed = SystemTime::now();
79        self.access_count += 1;
80    }
81
82    /// Estimate the memory size of the cached value
83    fn estimate_size(value: &serde_json::Value) -> usize {
84        // Rough estimation based on JSON serialization
85        match serde_json::to_string(value) {
86            Ok(s) => s.len(),
87            Err(_) => 0,
88        }
89    }
90
91    /// Get age of the entry
92    pub fn age(&self) -> Duration {
93        self.created_at.elapsed().unwrap_or(Duration::ZERO)
94    }
95
96    /// Get time since last access
97    pub fn idle_time(&self) -> Duration {
98        self.last_accessed.elapsed().unwrap_or(Duration::ZERO)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use serde_json::json;
106
107    #[test]
108    fn test_cache_key_creation() {
109        let inputs = json!({"x": 10, "y": 20});
110        let key1 = CacheKey::new("node1", &inputs);
111        let key2 = CacheKey::new("node1", &inputs);
112
113        assert_eq!(key1, key2);
114        assert_eq!(key1.node_id, "node1");
115    }
116
117    #[test]
118    fn test_cache_key_different_inputs() {
119        let inputs1 = json!({"x": 10});
120        let inputs2 = json!({"x": 20});
121
122        let key1 = CacheKey::new("node1", &inputs1);
123        let key2 = CacheKey::new("node1", &inputs2);
124
125        assert_ne!(key1.input_hash, key2.input_hash);
126    }
127
128    #[test]
129    fn test_cache_entry_expiration() {
130        let key = CacheKey::new("node1", &json!({}));
131        let mut entry = CacheEntry::new(key, json!({"result": 42}), Some(Duration::from_millis(1)));
132
133        assert!(!entry.is_expired());
134
135        std::thread::sleep(Duration::from_millis(10));
136        assert!(entry.is_expired());
137    }
138
139    #[test]
140    fn test_cache_entry_no_expiration() {
141        let key = CacheKey::new("node1", &json!({}));
142        let entry = CacheEntry::new(key, json!({"result": 42}), None);
143
144        assert!(!entry.is_expired());
145    }
146
147    #[test]
148    fn test_cache_entry_access_tracking() {
149        let key = CacheKey::new("node1", &json!({}));
150        let mut entry = CacheEntry::new(key, json!({"result": 42}), None);
151
152        assert_eq!(entry.access_count, 0);
153
154        entry.mark_accessed();
155        assert_eq!(entry.access_count, 1);
156
157        entry.mark_accessed();
158        assert_eq!(entry.access_count, 2);
159    }
160}