1use std::collections::BTreeMap;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicUsize, Ordering};
20use std::sync::Arc;
21
22use dashmap::DashMap;
23use parking_lot::Mutex;
24
25use chronix_core::{Point, SeriesKey};
26
27struct LruNode {
34 key: Arc<SeriesKey>,
35 prev: Option<usize>,
36 next: Option<usize>,
37}
38
39struct LruOrder {
44 nodes: Vec<LruNode>,
46 map: HashMap<Arc<SeriesKey>, usize>,
49 free: Vec<usize>,
51 head: Option<usize>,
53 tail: Option<usize>,
55}
56
57impl LruOrder {
58 #[cfg(test)]
60 fn len(&self) -> usize {
61 self.map.len()
62 }
63
64 fn new() -> Self {
65 Self {
66 nodes: Vec::new(),
67 map: HashMap::new(),
68 free: Vec::new(),
69 head: None,
70 tail: None,
71 }
72 }
73
74 fn with_capacity(cap: usize) -> Self {
75 Self {
76 nodes: Vec::with_capacity(cap),
77 map: HashMap::with_capacity(cap),
78 free: Vec::new(),
79 head: None,
80 tail: None,
81 }
82 }
83
84 fn alloc(&mut self, key: Arc<SeriesKey>) -> usize {
86 if let Some(idx) = self.free.pop() {
87 self.nodes[idx] = LruNode {
88 key,
89 prev: None,
90 next: None,
91 };
92 idx
93 } else {
94 let idx = self.nodes.len();
95 self.nodes.push(LruNode {
96 key,
97 prev: None,
98 next: None,
99 });
100 idx
101 }
102 }
103
104 fn unlink(&mut self, idx: usize) {
106 let prev = self.nodes[idx].prev;
107 let next = self.nodes[idx].next;
108
109 match prev {
110 Some(p) => self.nodes[p].next = next,
111 None => self.head = next,
112 }
113 match next {
114 Some(n) => self.nodes[n].prev = prev,
115 None => self.tail = prev,
116 }
117
118 self.nodes[idx].prev = None;
119 self.nodes[idx].next = None;
120 }
121
122 fn link_tail(&mut self, idx: usize) {
124 self.nodes[idx].prev = self.tail;
125 self.nodes[idx].next = None;
126
127 if let Some(old_tail) = self.tail {
128 self.nodes[old_tail].next = Some(idx);
129 }
130 self.tail = Some(idx);
131 if self.head.is_none() {
132 self.head = Some(idx);
133 }
134 }
135
136 fn touch(&mut self, key: &SeriesKey) {
138 if let Some(&idx) = self.map.get(key) {
139 self.unlink(idx);
141 self.link_tail(idx);
142 } else {
143 let arc = Arc::new(key.clone());
145 let idx = self.alloc(Arc::clone(&arc));
146 self.map.insert(arc, idx);
147 self.link_tail(idx);
148 }
149 }
150
151 fn pop_front(&mut self) -> Option<Arc<SeriesKey>> {
153 let idx = self.head?;
154 self.unlink(idx);
155 let key = self.nodes[idx].key.clone();
156 self.map.remove(&key);
157 self.free.push(idx);
158 Some(key)
159 }
160
161 fn remove(&mut self, key: &SeriesKey) {
163 if let Some(idx) = self.map.remove(key) {
164 self.unlink(idx);
165 self.free.push(idx);
166 }
167 }
168
169 fn retain_present(&mut self, cache: &DashMap<SeriesKey, Point>) {
171 let to_remove: Vec<Arc<SeriesKey>> = self
172 .map
173 .keys()
174 .filter(|k| !cache.contains_key(k.as_ref()))
175 .cloned()
176 .collect();
177 for key in to_remove {
178 self.remove(&key);
179 }
180 }
181
182 fn clear(&mut self) {
184 self.nodes.clear();
185 self.map.clear();
186 self.free.clear();
187 self.head = None;
188 self.tail = None;
189 }
190}
191
192pub struct LastValueCache {
214 cache: DashMap<SeriesKey, Point>,
216 update_count: AtomicUsize,
218 max_entries: usize,
220 order: Mutex<LruOrder>,
222}
223
224impl LastValueCache {
225 #[must_use]
227 pub fn new() -> Self {
228 Self {
229 cache: DashMap::new(),
230 update_count: AtomicUsize::new(0),
231 max_entries: 0,
232 order: Mutex::new(LruOrder::new()),
233 }
234 }
235
236 #[must_use]
241 pub fn with_max_entries(max: usize) -> Self {
242 Self {
243 cache: DashMap::with_capacity(max.min(1024)),
244 update_count: AtomicUsize::new(0),
245 max_entries: max,
246 order: Mutex::new(LruOrder::with_capacity(max.min(1024))),
247 }
248 }
249
250 #[must_use]
252 pub fn with_capacity(n: usize) -> Self {
253 Self {
254 cache: DashMap::with_capacity(n),
255 update_count: AtomicUsize::new(0),
256 max_entries: 0,
257 order: Mutex::new(LruOrder::new()),
258 }
259 }
260
261 pub fn update(&self, point: &Point) {
268 let key = point.series_key().clone();
269
270 if self.max_entries > 0 {
272 let mut order = self.order.lock();
273 let is_new = !self.cache.contains_key(&key);
274
275 order.touch(&key);
278
279 while is_new && self.cache.len() >= self.max_entries {
281 if let Some(evict_key) = order.pop_front() {
282 self.cache.remove(&evict_key);
283 } else {
284 break;
285 }
286 }
287 }
288
289 self.cache
290 .entry(key)
291 .and_modify(|existing| {
292 if point.timestamp() >= existing.timestamp() {
293 *existing = point.clone();
294 }
295 })
296 .or_insert_with(|| point.clone());
297
298 self.update_count.fetch_add(1, Ordering::Relaxed);
299 }
300
301 #[must_use]
306 pub fn get(&self, measurement: &str, tags: &BTreeMap<String, String>) -> Option<Point> {
307 let key = SeriesKey::new(measurement, tags.clone()).ok()?;
308 self.cache.get(&key).map(|entry| entry.value().clone())
309 }
310
311 #[must_use]
313 pub fn get_by_key(&self, key: &SeriesKey) -> Option<Point> {
314 self.cache.get(key).map(|entry| entry.value().clone())
315 }
316
317 pub fn evict(&self, measurement: &str, tags: &BTreeMap<String, String>) {
319 if let Ok(key) = SeriesKey::new(measurement, tags.clone()) {
320 self.cache.remove(&key);
321 if self.max_entries > 0 {
322 self.order.lock().remove(&key);
323 }
324 }
325 }
326
327 pub fn evict_by_key(&self, key: &SeriesKey) {
329 self.cache.remove(key);
330 if self.max_entries > 0 {
331 self.order.lock().remove(key);
332 }
333 }
334
335 pub fn evict_measurement(&self, measurement: &str) {
337 self.cache
338 .retain(|key, _point| key.measurement() != measurement);
339 if self.max_entries > 0 {
340 self.order.lock().retain_present(&self.cache);
341 }
342 }
343
344 pub fn clear(&self) {
346 self.cache.clear();
347 if self.max_entries > 0 {
348 self.order.lock().clear();
349 }
350 }
351
352 #[must_use]
354 pub fn len(&self) -> usize {
355 self.cache.len()
356 }
357
358 #[must_use]
360 pub fn is_empty(&self) -> bool {
361 self.cache.is_empty()
362 }
363
364 #[must_use]
366 pub fn update_count(&self) -> usize {
367 self.update_count.load(Ordering::Relaxed)
368 }
369}
370
371impl Default for LastValueCache {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377impl std::fmt::Debug for LastValueCache {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379 f.debug_struct("LastValueCache")
380 .field("entries", &self.cache.len())
381 .field("updates", &self.update_count.load(Ordering::Relaxed))
382 .finish()
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use chronix_core::FieldValue;
390
391 fn make_point(host: &str, value: f64, ts: i64) -> Point {
392 let tags = BTreeMap::from([("host".to_string(), host.to_string())]);
393 let fields = BTreeMap::from([("cpu".to_string(), FieldValue::F64(value))]);
394 let key = SeriesKey::new("cpu", tags).unwrap();
395 Point::new(key, fields, ts).unwrap()
396 }
397
398 #[test]
399 fn update_and_get() {
400 let lvc = LastValueCache::new();
401 let p = make_point("srv1", 42.0, 1000);
402 lvc.update(&p);
403
404 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
405 let result = lvc.get("cpu", &tags);
406 assert!(result.is_some());
407 assert_eq!(result.unwrap().timestamp(), 1000);
408 }
409
410 #[test]
411 fn newer_overwrites_older() {
412 let lvc = LastValueCache::new();
413 lvc.update(&make_point("srv1", 10.0, 100));
414 lvc.update(&make_point("srv1", 20.0, 200));
415
416 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
417 let result = lvc.get("cpu", &tags).unwrap();
418 assert_eq!(result.timestamp(), 200);
419 }
420
421 #[test]
422 fn older_does_not_overwrite() {
423 let lvc = LastValueCache::new();
424 lvc.update(&make_point("srv1", 20.0, 200));
425 lvc.update(&make_point("srv1", 10.0, 100));
426
427 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
428 let result = lvc.get("cpu", &tags).unwrap();
429 assert_eq!(result.timestamp(), 200);
430 }
431
432 #[test]
433 fn get_nonexistent_returns_none() {
434 let lvc = LastValueCache::new();
435 let tags = BTreeMap::from([("host".to_string(), "ghost".to_string())]);
436 assert!(lvc.get("cpu", &tags).is_none());
437 }
438
439 #[test]
440 fn evict_removes_entry() {
441 let lvc = LastValueCache::new();
442 lvc.update(&make_point("srv1", 1.0, 100));
443 assert_eq!(lvc.len(), 1);
444
445 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
446 lvc.evict("cpu", &tags);
447 assert!(lvc.get("cpu", &tags).is_none());
448 assert_eq!(lvc.len(), 0);
449 }
450
451 #[test]
452 fn evict_measurement_removes_all_series() {
453 let lvc = LastValueCache::new();
454 lvc.update(&make_point("srv1", 1.0, 100));
455 lvc.update(&make_point("srv2", 2.0, 100));
456 assert_eq!(lvc.len(), 2);
457
458 lvc.evict_measurement("cpu");
459 assert_eq!(lvc.len(), 0);
460 }
461
462 #[test]
463 fn clear_empties_cache() {
464 let lvc = LastValueCache::new();
465 lvc.update(&make_point("srv1", 1.0, 100));
466 lvc.update(&make_point("srv2", 2.0, 100));
467 lvc.clear();
468 assert!(lvc.is_empty());
469 }
470
471 #[test]
472 fn update_count_tracks_operations() {
473 let lvc = LastValueCache::new();
474 assert_eq!(lvc.update_count(), 0);
475 lvc.update(&make_point("srv1", 1.0, 100));
476 lvc.update(&make_point("srv1", 2.0, 200));
477 assert_eq!(lvc.update_count(), 2);
478 }
479
480 #[test]
481 fn multiple_series_independent() {
482 let lvc = LastValueCache::new();
483 lvc.update(&make_point("srv1", 1.0, 100));
484 lvc.update(&make_point("srv2", 2.0, 200));
485
486 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
487 let tags2 = BTreeMap::from([("host".to_string(), "srv2".to_string())]);
488
489 assert_eq!(lvc.get("cpu", &tags1).unwrap().timestamp(), 100);
490 assert_eq!(lvc.get("cpu", &tags2).unwrap().timestamp(), 200);
491 }
492
493 #[test]
494 fn get_by_key() {
495 let lvc = LastValueCache::new();
496 let p = make_point("srv1", 42.0, 1000);
497 let key = p.series_key().clone();
498 lvc.update(&p);
499
500 assert!(lvc.get_by_key(&key).is_some());
501 let ghost_key = SeriesKey::new(
502 "cpu",
503 BTreeMap::from([("host".to_string(), "ghost".to_string())]),
504 )
505 .unwrap();
506 assert!(lvc.get_by_key(&ghost_key).is_none());
507 }
508
509 #[test]
512 fn lru_eviction_respects_max_entries() {
513 let lvc = LastValueCache::with_max_entries(2);
514 lvc.update(&make_point("srv1", 1.0, 100));
515 lvc.update(&make_point("srv2", 2.0, 200));
516 assert_eq!(lvc.len(), 2);
517
518 lvc.update(&make_point("srv3", 3.0, 300));
520 assert_eq!(lvc.len(), 2);
521
522 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
523 let tags3 = BTreeMap::from([("host".to_string(), "srv3".to_string())]);
524 assert!(lvc.get("cpu", &tags1).is_none(), "srv1 should be evicted");
525 assert!(lvc.get("cpu", &tags3).is_some(), "srv3 should be present");
526 }
527
528 #[test]
529 fn lru_touch_moves_to_back() {
530 let lvc = LastValueCache::with_max_entries(2);
531 lvc.update(&make_point("srv1", 1.0, 100));
532 lvc.update(&make_point("srv2", 2.0, 200));
533
534 lvc.update(&make_point("srv1", 1.5, 300));
536
537 lvc.update(&make_point("srv3", 3.0, 400));
539
540 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
541 let tags2 = BTreeMap::from([("host".to_string(), "srv2".to_string())]);
542 let tags3 = BTreeMap::from([("host".to_string(), "srv3".to_string())]);
543 assert!(
544 lvc.get("cpu", &tags1).is_some(),
545 "srv1 was touched, should survive"
546 );
547 assert!(
548 lvc.get("cpu", &tags2).is_none(),
549 "srv2 should be evicted (LRU)"
550 );
551 assert!(lvc.get("cpu", &tags3).is_some(), "srv3 should be present");
552 }
553
554 #[test]
555 fn lru_evict_by_key_updates_order() {
556 let lvc = LastValueCache::with_max_entries(3);
557 lvc.update(&make_point("srv1", 1.0, 100));
558 lvc.update(&make_point("srv2", 2.0, 200));
559 lvc.update(&make_point("srv3", 3.0, 300));
560
561 let key2 = make_point("srv2", 0.0, 0).series_key().clone();
563 lvc.evict_by_key(&key2);
564 assert_eq!(lvc.len(), 2);
565 assert_eq!(lvc.order.lock().len(), 2);
566 }
567
568 #[test]
569 fn lru_order_internal_consistency() {
570 let lvc = LastValueCache::with_max_entries(100);
571 for i in 0..50 {
573 lvc.update(&make_point(&format!("s{i}"), i as f64, i as i64));
574 }
575 assert_eq!(lvc.len(), 50);
576 assert_eq!(lvc.order.lock().len(), 50);
577
578 for i in 0..25 {
580 lvc.update(&make_point(&format!("s{i}"), i as f64, 1000 + i as i64));
581 }
582 assert_eq!(lvc.len(), 50);
583 assert_eq!(lvc.order.lock().len(), 50);
584
585 lvc.evict_measurement("cpu");
587 assert_eq!(lvc.len(), 0);
588 assert_eq!(lvc.order.lock().len(), 0);
589 }
590}