Skip to main content

mabi_opcua/nodes/
cache.rs

1//! Node caching for large address spaces.
2//!
3//! This module provides LRU caching for frequently accessed nodes,
4//! improving performance when dealing with 100,000+ node address spaces.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use lru::LruCache;
10use parking_lot::Mutex;
11use serde::{Deserialize, Serialize};
12use tracing::{debug, trace};
13
14use crate::types::{NodeId, DataValue};
15use super::base::{NodeClass, QualifiedName, LocalizedText};
16use super::reference::ReferenceDescription;
17
18/// Node cache configuration.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct NodeCacheConfig {
21    /// Maximum number of nodes to cache.
22    pub max_size: usize,
23    /// Whether to enable prefetching.
24    pub prefetch_enabled: bool,
25    /// Prefetch depth (how many levels to prefetch).
26    pub prefetch_depth: usize,
27    /// Enable value caching for variables.
28    pub cache_values: bool,
29    /// Value cache TTL in milliseconds.
30    pub value_cache_ttl_ms: u64,
31}
32
33impl Default for NodeCacheConfig {
34    fn default() -> Self {
35        Self {
36            max_size: 100_000,
37            prefetch_enabled: true,
38            prefetch_depth: 2,
39            cache_values: true,
40            value_cache_ttl_ms: 1000, // 1 second
41        }
42    }
43}
44
45/// Cache statistics.
46#[derive(Debug, Clone, Default)]
47pub struct CacheStats {
48    /// Cache hits.
49    pub hits: u64,
50    /// Cache misses.
51    pub misses: u64,
52    /// Cache evictions.
53    pub evictions: u64,
54    /// Prefetch operations.
55    pub prefetches: u64,
56    /// Current cache size.
57    pub size: usize,
58}
59
60impl CacheStats {
61    /// Calculate hit rate.
62    pub fn hit_rate(&self) -> f64 {
63        let total = self.hits + self.misses;
64        if total == 0 {
65            return 0.0;
66        }
67        self.hits as f64 / total as f64
68    }
69}
70
71/// Cached node entry.
72#[derive(Debug, Clone)]
73pub struct CachedNode {
74    /// Node ID.
75    pub node_id: NodeId,
76    /// Node class.
77    pub node_class: NodeClass,
78    /// Browse name.
79    pub browse_name: QualifiedName,
80    /// Display name.
81    pub display_name: LocalizedText,
82    /// Cached value (for variables).
83    pub value: Option<CachedValue>,
84    /// Cached references.
85    pub references: Option<Vec<ReferenceDescription>>,
86}
87
88/// Cached variable value.
89#[derive(Debug, Clone)]
90pub struct CachedValue {
91    /// The data value.
92    pub data_value: DataValue,
93    /// Timestamp when cached.
94    pub cached_at: std::time::Instant,
95}
96
97impl CachedValue {
98    /// Create a new cached value.
99    pub fn new(data_value: DataValue) -> Self {
100        Self {
101            data_value,
102            cached_at: std::time::Instant::now(),
103        }
104    }
105
106    /// Check if the cached value is expired.
107    pub fn is_expired(&self, ttl_ms: u64) -> bool {
108        self.cached_at.elapsed().as_millis() > ttl_ms as u128
109    }
110}
111
112/// Node cache for efficient node access.
113///
114/// Uses an LRU eviction policy to maintain a bounded cache size.
115/// Thread-safe through internal locking.
116///
117/// # Examples
118///
119/// ```ignore
120/// let cache = NodeCache::new(NodeCacheConfig::default());
121///
122/// // Put a node
123/// cache.put(CachedNode {
124///     node_id: NodeId::numeric(2, 1001),
125///     node_class: NodeClass::Variable,
126///     browse_name: QualifiedName::new(2, "Temperature"),
127///     display_name: LocalizedText::invariant("Temperature"),
128///     value: None,
129///     references: None,
130/// });
131///
132/// // Get a node
133/// if let Some(node) = cache.get(&NodeId::numeric(2, 1001)) {
134///     println!("Found: {}", node.display_name);
135/// }
136/// ```
137pub struct NodeCache {
138    config: NodeCacheConfig,
139    cache: Mutex<LruCache<NodeId, CachedNode>>,
140    hits: AtomicU64,
141    misses: AtomicU64,
142    evictions: AtomicU64,
143    prefetches: AtomicU64,
144}
145
146impl NodeCache {
147    /// Create a new node cache.
148    pub fn new(config: NodeCacheConfig) -> Self {
149        let max_size = std::num::NonZeroUsize::new(config.max_size)
150            .unwrap_or(std::num::NonZeroUsize::new(1000).unwrap());
151
152        Self {
153            config,
154            cache: Mutex::new(LruCache::new(max_size)),
155            hits: AtomicU64::new(0),
156            misses: AtomicU64::new(0),
157            evictions: AtomicU64::new(0),
158            prefetches: AtomicU64::new(0),
159        }
160    }
161
162    /// Get a node from cache.
163    pub fn get(&self, node_id: &NodeId) -> Option<CachedNode> {
164        let mut cache = self.cache.lock();
165
166        if let Some(node) = cache.get(node_id) {
167            self.hits.fetch_add(1, Ordering::Relaxed);
168            trace!(node_id = %node_id, "Cache hit");
169            Some(node.clone())
170        } else {
171            self.misses.fetch_add(1, Ordering::Relaxed);
172            trace!(node_id = %node_id, "Cache miss");
173            None
174        }
175    }
176
177    /// Get a node's cached value if not expired.
178    pub fn get_value(&self, node_id: &NodeId) -> Option<DataValue> {
179        if !self.config.cache_values {
180            return None;
181        }
182
183        let cache = self.cache.lock();
184
185        if let Some(node) = cache.peek(node_id) {
186            if let Some(ref cached_value) = node.value {
187                if !cached_value.is_expired(self.config.value_cache_ttl_ms) {
188                    return Some(cached_value.data_value.clone());
189                }
190            }
191        }
192
193        None
194    }
195
196    /// Put a node into cache.
197    pub fn put(&self, node: CachedNode) {
198        let mut cache = self.cache.lock();
199
200        // Check if we need to evict
201        if cache.len() >= self.config.max_size {
202            if cache.pop_lru().is_some() {
203                self.evictions.fetch_add(1, Ordering::Relaxed);
204            }
205        }
206
207        trace!(node_id = %node.node_id, "Cache put");
208        cache.put(node.node_id.clone(), node);
209    }
210
211    /// Update a cached node's value.
212    pub fn update_value(&self, node_id: &NodeId, data_value: DataValue) {
213        if !self.config.cache_values {
214            return;
215        }
216
217        let mut cache = self.cache.lock();
218
219        if let Some(node) = cache.get_mut(node_id) {
220            node.value = Some(CachedValue::new(data_value));
221        }
222    }
223
224    /// Update a cached node's references.
225    pub fn update_references(&self, node_id: &NodeId, references: Vec<ReferenceDescription>) {
226        let mut cache = self.cache.lock();
227
228        if let Some(node) = cache.get_mut(node_id) {
229            node.references = Some(references);
230        }
231    }
232
233    /// Invalidate a cached node.
234    pub fn invalidate(&self, node_id: &NodeId) {
235        let mut cache = self.cache.lock();
236        cache.pop(node_id);
237        debug!(node_id = %node_id, "Cache invalidate");
238    }
239
240    /// Invalidate all cached nodes.
241    pub fn clear(&self) {
242        let mut cache = self.cache.lock();
243        cache.clear();
244        debug!("Cache cleared");
245    }
246
247    /// Get cache statistics.
248    pub fn stats(&self) -> CacheStats {
249        let cache = self.cache.lock();
250        CacheStats {
251            hits: self.hits.load(Ordering::Relaxed),
252            misses: self.misses.load(Ordering::Relaxed),
253            evictions: self.evictions.load(Ordering::Relaxed),
254            prefetches: self.prefetches.load(Ordering::Relaxed),
255            size: cache.len(),
256        }
257    }
258
259    /// Check if a node is cached.
260    pub fn contains(&self, node_id: &NodeId) -> bool {
261        let cache = self.cache.lock();
262        cache.contains(node_id)
263    }
264
265    /// Get current cache size.
266    pub fn len(&self) -> usize {
267        self.cache.lock().len()
268    }
269
270    /// Check if cache is empty.
271    pub fn is_empty(&self) -> bool {
272        self.cache.lock().is_empty()
273    }
274
275    /// Get the cache configuration.
276    pub fn config(&self) -> &NodeCacheConfig {
277        &self.config
278    }
279
280    /// Record a prefetch operation.
281    pub fn record_prefetch(&self, count: usize) {
282        self.prefetches.fetch_add(count as u64, Ordering::Relaxed);
283    }
284}
285
286impl Default for NodeCache {
287    fn default() -> Self {
288        Self::new(NodeCacheConfig::default())
289    }
290}
291
292/// Cache warming utility for preloading nodes.
293pub struct CacheWarmer {
294    cache: Arc<NodeCache>,
295}
296
297impl CacheWarmer {
298    /// Create a new cache warmer.
299    pub fn new(cache: Arc<NodeCache>) -> Self {
300        Self { cache }
301    }
302
303    /// Warm the cache with a list of nodes.
304    pub fn warm(&self, nodes: impl IntoIterator<Item = CachedNode>) {
305        let count = nodes.into_iter().map(|node| {
306            self.cache.put(node);
307        }).count();
308
309        debug!(count, "Cache warmed");
310    }
311
312    /// Warm the cache with frequently accessed node IDs.
313    /// This is a hint for prefetching from the address space.
314    pub fn warm_hints(&self, _hints: &[NodeId]) {
315        // This would be implemented to fetch from address space
316        // and populate cache
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::types::Variant;
324
325    fn create_test_node(id: u32) -> CachedNode {
326        CachedNode {
327            node_id: NodeId::numeric(2, id),
328            node_class: NodeClass::Variable,
329            browse_name: QualifiedName::new(2, format!("Node{}", id)),
330            display_name: LocalizedText::invariant(format!("Node {}", id)),
331            value: None,
332            references: None,
333        }
334    }
335
336    #[test]
337    fn test_cache_basic() {
338        let cache = NodeCache::new(NodeCacheConfig {
339            max_size: 10,
340            ..Default::default()
341        });
342
343        // Put a node
344        let node = create_test_node(1001);
345        cache.put(node.clone());
346
347        // Get it back
348        let retrieved = cache.get(&NodeId::numeric(2, 1001)).unwrap();
349        assert_eq!(retrieved.node_id, node.node_id);
350    }
351
352    #[test]
353    fn test_cache_miss() {
354        let cache = NodeCache::new(NodeCacheConfig::default());
355
356        let result = cache.get(&NodeId::numeric(2, 9999));
357        assert!(result.is_none());
358
359        let stats = cache.stats();
360        assert_eq!(stats.misses, 1);
361    }
362
363    #[test]
364    fn test_cache_eviction() {
365        let cache = NodeCache::new(NodeCacheConfig {
366            max_size: 5,
367            ..Default::default()
368        });
369
370        // Fill cache beyond capacity
371        for i in 0..10 {
372            cache.put(create_test_node(1000 + i));
373        }
374
375        assert_eq!(cache.len(), 5);
376
377        let stats = cache.stats();
378        assert!(stats.evictions >= 5);
379    }
380
381    #[test]
382    fn test_cache_invalidate() {
383        let cache = NodeCache::new(NodeCacheConfig::default());
384
385        cache.put(create_test_node(1001));
386        assert!(cache.contains(&NodeId::numeric(2, 1001)));
387
388        cache.invalidate(&NodeId::numeric(2, 1001));
389        assert!(!cache.contains(&NodeId::numeric(2, 1001)));
390    }
391
392    #[test]
393    fn test_cache_clear() {
394        let cache = NodeCache::new(NodeCacheConfig::default());
395
396        for i in 0..10 {
397            cache.put(create_test_node(1000 + i));
398        }
399
400        assert_eq!(cache.len(), 10);
401
402        cache.clear();
403        assert!(cache.is_empty());
404    }
405
406    #[test]
407    fn test_cached_value_expiration() {
408        let value = CachedValue::new(DataValue::new(Variant::double(25.5)));
409
410        // Should not be expired immediately
411        assert!(!value.is_expired(1000));
412
413        // After artificial delay, should be expired
414        // (In real tests, we'd use mock time)
415    }
416
417    #[test]
418    fn test_cache_stats() {
419        let cache = NodeCache::new(NodeCacheConfig::default());
420
421        // Some hits and misses
422        cache.put(create_test_node(1001));
423        cache.get(&NodeId::numeric(2, 1001)); // hit
424        cache.get(&NodeId::numeric(2, 1001)); // hit
425        cache.get(&NodeId::numeric(2, 9999)); // miss
426
427        let stats = cache.stats();
428        assert_eq!(stats.hits, 2);
429        assert_eq!(stats.misses, 1);
430        assert!((stats.hit_rate() - 0.666).abs() < 0.01);
431    }
432}