1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct NodeCacheConfig {
21 pub max_size: usize,
23 pub prefetch_enabled: bool,
25 pub prefetch_depth: usize,
27 pub cache_values: bool,
29 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, }
42 }
43}
44
45#[derive(Debug, Clone, Default)]
47pub struct CacheStats {
48 pub hits: u64,
50 pub misses: u64,
52 pub evictions: u64,
54 pub prefetches: u64,
56 pub size: usize,
58}
59
60impl CacheStats {
61 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#[derive(Debug, Clone)]
73pub struct CachedNode {
74 pub node_id: NodeId,
76 pub node_class: NodeClass,
78 pub browse_name: QualifiedName,
80 pub display_name: LocalizedText,
82 pub value: Option<CachedValue>,
84 pub references: Option<Vec<ReferenceDescription>>,
86}
87
88#[derive(Debug, Clone)]
90pub struct CachedValue {
91 pub data_value: DataValue,
93 pub cached_at: std::time::Instant,
95}
96
97impl CachedValue {
98 pub fn new(data_value: DataValue) -> Self {
100 Self {
101 data_value,
102 cached_at: std::time::Instant::now(),
103 }
104 }
105
106 pub fn is_expired(&self, ttl_ms: u64) -> bool {
108 self.cached_at.elapsed().as_millis() > ttl_ms as u128
109 }
110}
111
112pub 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 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 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 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 pub fn put(&self, node: CachedNode) {
198 let mut cache = self.cache.lock();
199
200 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 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 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 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 pub fn clear(&self) {
242 let mut cache = self.cache.lock();
243 cache.clear();
244 debug!("Cache cleared");
245 }
246
247 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 pub fn contains(&self, node_id: &NodeId) -> bool {
261 let cache = self.cache.lock();
262 cache.contains(node_id)
263 }
264
265 pub fn len(&self) -> usize {
267 self.cache.lock().len()
268 }
269
270 pub fn is_empty(&self) -> bool {
272 self.cache.lock().is_empty()
273 }
274
275 pub fn config(&self) -> &NodeCacheConfig {
277 &self.config
278 }
279
280 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
292pub struct CacheWarmer {
294 cache: Arc<NodeCache>,
295}
296
297impl CacheWarmer {
298 pub fn new(cache: Arc<NodeCache>) -> Self {
300 Self { cache }
301 }
302
303 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 pub fn warm_hints(&self, _hints: &[NodeId]) {
315 }
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 let node = create_test_node(1001);
345 cache.put(node.clone());
346
347 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 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 assert!(!value.is_expired(1000));
412
413 }
416
417 #[test]
418 fn test_cache_stats() {
419 let cache = NodeCache::new(NodeCacheConfig::default());
420
421 cache.put(create_test_node(1001));
423 cache.get(&NodeId::numeric(2, 1001)); cache.get(&NodeId::numeric(2, 1001)); cache.get(&NodeId::numeric(2, 9999)); 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}