1use std::collections::BTreeMap;
18use std::collections::HashMap;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicUsize, Ordering};
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 retain_series(&self, keep: impl Fn(&str) -> bool) {
349 self.cache.retain(|key, _point| keep(key.canonical_form()));
350 if self.max_entries > 0 {
351 self.order.lock().retain_present(&self.cache);
352 }
353 }
354
355 pub fn evict_measurement(&self, measurement: &str) {
357 self.cache
358 .retain(|key, _point| key.measurement() != measurement);
359 if self.max_entries > 0 {
360 self.order.lock().retain_present(&self.cache);
361 }
362 }
363
364 pub fn clear(&self) {
366 self.cache.clear();
367 if self.max_entries > 0 {
368 self.order.lock().clear();
369 }
370 }
371
372 #[must_use]
374 pub fn len(&self) -> usize {
375 self.cache.len()
376 }
377
378 #[must_use]
380 pub fn is_empty(&self) -> bool {
381 self.cache.is_empty()
382 }
383
384 #[must_use]
386 pub fn update_count(&self) -> usize {
387 self.update_count.load(Ordering::Relaxed)
388 }
389}
390
391impl Default for LastValueCache {
392 fn default() -> Self {
393 Self::new()
394 }
395}
396
397impl std::fmt::Debug for LastValueCache {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 f.debug_struct("LastValueCache")
400 .field("entries", &self.cache.len())
401 .field("updates", &self.update_count.load(Ordering::Relaxed))
402 .finish()
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use chronix_core::FieldValue;
410
411 fn make_point(host: &str, value: f64, ts: i64) -> Point {
412 let tags = BTreeMap::from([("host".to_string(), host.to_string())]);
413 let fields = BTreeMap::from([("cpu".to_string(), FieldValue::F64(value))]);
414 let key = SeriesKey::new("cpu", tags).unwrap();
415 Point::new(key, fields, ts).unwrap()
416 }
417
418 #[test]
419 fn update_and_get() {
420 let lvc = LastValueCache::new();
421 let p = make_point("srv1", 42.0, 1000);
422 lvc.update(&p);
423
424 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
425 let result = lvc.get("cpu", &tags);
426 assert!(result.is_some());
427 assert_eq!(result.unwrap().timestamp(), 1000);
428 }
429
430 #[test]
431 fn newer_overwrites_older() {
432 let lvc = LastValueCache::new();
433 lvc.update(&make_point("srv1", 10.0, 100));
434 lvc.update(&make_point("srv1", 20.0, 200));
435
436 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
437 let result = lvc.get("cpu", &tags).unwrap();
438 assert_eq!(result.timestamp(), 200);
439 }
440
441 #[test]
442 fn older_does_not_overwrite() {
443 let lvc = LastValueCache::new();
444 lvc.update(&make_point("srv1", 20.0, 200));
445 lvc.update(&make_point("srv1", 10.0, 100));
446
447 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
448 let result = lvc.get("cpu", &tags).unwrap();
449 assert_eq!(result.timestamp(), 200);
450 }
451
452 #[test]
453 fn get_nonexistent_returns_none() {
454 let lvc = LastValueCache::new();
455 let tags = BTreeMap::from([("host".to_string(), "ghost".to_string())]);
456 assert!(lvc.get("cpu", &tags).is_none());
457 }
458
459 #[test]
460 fn evict_removes_entry() {
461 let lvc = LastValueCache::new();
462 lvc.update(&make_point("srv1", 1.0, 100));
463 assert_eq!(lvc.len(), 1);
464
465 let tags = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
466 lvc.evict("cpu", &tags);
467 assert!(lvc.get("cpu", &tags).is_none());
468 assert_eq!(lvc.len(), 0);
469 }
470
471 #[test]
472 fn evict_measurement_removes_all_series() {
473 let lvc = LastValueCache::new();
474 lvc.update(&make_point("srv1", 1.0, 100));
475 lvc.update(&make_point("srv2", 2.0, 100));
476 assert_eq!(lvc.len(), 2);
477
478 lvc.evict_measurement("cpu");
479 assert_eq!(lvc.len(), 0);
480 }
481
482 #[test]
483 fn clear_empties_cache() {
484 let lvc = LastValueCache::new();
485 lvc.update(&make_point("srv1", 1.0, 100));
486 lvc.update(&make_point("srv2", 2.0, 100));
487 lvc.clear();
488 assert!(lvc.is_empty());
489 }
490
491 #[test]
492 fn update_count_tracks_operations() {
493 let lvc = LastValueCache::new();
494 assert_eq!(lvc.update_count(), 0);
495 lvc.update(&make_point("srv1", 1.0, 100));
496 lvc.update(&make_point("srv1", 2.0, 200));
497 assert_eq!(lvc.update_count(), 2);
498 }
499
500 #[test]
501 fn multiple_series_independent() {
502 let lvc = LastValueCache::new();
503 lvc.update(&make_point("srv1", 1.0, 100));
504 lvc.update(&make_point("srv2", 2.0, 200));
505
506 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
507 let tags2 = BTreeMap::from([("host".to_string(), "srv2".to_string())]);
508
509 assert_eq!(lvc.get("cpu", &tags1).unwrap().timestamp(), 100);
510 assert_eq!(lvc.get("cpu", &tags2).unwrap().timestamp(), 200);
511 }
512
513 #[test]
514 fn get_by_key() {
515 let lvc = LastValueCache::new();
516 let p = make_point("srv1", 42.0, 1000);
517 let key = p.series_key().clone();
518 lvc.update(&p);
519
520 assert!(lvc.get_by_key(&key).is_some());
521 let ghost_key = SeriesKey::new(
522 "cpu",
523 BTreeMap::from([("host".to_string(), "ghost".to_string())]),
524 )
525 .unwrap();
526 assert!(lvc.get_by_key(&ghost_key).is_none());
527 }
528
529 #[test]
532 fn lru_eviction_respects_max_entries() {
533 let lvc = LastValueCache::with_max_entries(2);
534 lvc.update(&make_point("srv1", 1.0, 100));
535 lvc.update(&make_point("srv2", 2.0, 200));
536 assert_eq!(lvc.len(), 2);
537
538 lvc.update(&make_point("srv3", 3.0, 300));
540 assert_eq!(lvc.len(), 2);
541
542 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
543 let tags3 = BTreeMap::from([("host".to_string(), "srv3".to_string())]);
544 assert!(lvc.get("cpu", &tags1).is_none(), "srv1 should be evicted");
545 assert!(lvc.get("cpu", &tags3).is_some(), "srv3 should be present");
546 }
547
548 #[test]
549 fn lru_touch_moves_to_back() {
550 let lvc = LastValueCache::with_max_entries(2);
551 lvc.update(&make_point("srv1", 1.0, 100));
552 lvc.update(&make_point("srv2", 2.0, 200));
553
554 lvc.update(&make_point("srv1", 1.5, 300));
556
557 lvc.update(&make_point("srv3", 3.0, 400));
559
560 let tags1 = BTreeMap::from([("host".to_string(), "srv1".to_string())]);
561 let tags2 = BTreeMap::from([("host".to_string(), "srv2".to_string())]);
562 let tags3 = BTreeMap::from([("host".to_string(), "srv3".to_string())]);
563 assert!(
564 lvc.get("cpu", &tags1).is_some(),
565 "srv1 was touched, should survive"
566 );
567 assert!(
568 lvc.get("cpu", &tags2).is_none(),
569 "srv2 should be evicted (LRU)"
570 );
571 assert!(lvc.get("cpu", &tags3).is_some(), "srv3 should be present");
572 }
573
574 #[test]
575 fn lru_evict_by_key_updates_order() {
576 let lvc = LastValueCache::with_max_entries(3);
577 lvc.update(&make_point("srv1", 1.0, 100));
578 lvc.update(&make_point("srv2", 2.0, 200));
579 lvc.update(&make_point("srv3", 3.0, 300));
580
581 let key2 = make_point("srv2", 0.0, 0).series_key().clone();
583 lvc.evict_by_key(&key2);
584 assert_eq!(lvc.len(), 2);
585 assert_eq!(lvc.order.lock().len(), 2);
586 }
587
588 #[test]
589 fn lru_order_internal_consistency() {
590 let lvc = LastValueCache::with_max_entries(100);
591 for i in 0..50 {
593 lvc.update(&make_point(&format!("s{i}"), i as f64, i as i64));
594 }
595 assert_eq!(lvc.len(), 50);
596 assert_eq!(lvc.order.lock().len(), 50);
597
598 for i in 0..25 {
600 lvc.update(&make_point(&format!("s{i}"), i as f64, 1000 + i as i64));
601 }
602 assert_eq!(lvc.len(), 50);
603 assert_eq!(lvc.order.lock().len(), 50);
604
605 lvc.evict_measurement("cpu");
607 assert_eq!(lvc.len(), 0);
608 assert_eq!(lvc.order.lock().len(), 0);
609 }
610}