1use std::collections::HashMap;
14use std::fmt;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use arrow::array::ArrayRef;
19use parking_lot::{Mutex, RwLock};
20use tokio::sync::Notify;
21
22use chronix_core::SegmentId;
23
24const SKETCH_DEPTH: usize = 4;
28const DEFAULT_SKETCH_WIDTH: usize = 2048;
30
31struct FrequencySketch {
41 table: Vec<Vec<u16>>,
43 width: usize,
45 additions: u64,
47 reset_interval: u64,
49}
50
51impl FrequencySketch {
52 #[cfg(test)]
54 fn new() -> Self {
55 Self::with_width(DEFAULT_SKETCH_WIDTH)
56 }
57
58 fn with_width(width: usize) -> Self {
60 let width = width.next_power_of_two().max(64);
61 Self {
62 table: vec![vec![0u16; width]; SKETCH_DEPTH],
63 width,
64 additions: 0,
65 reset_interval: 10 * width as u64,
66 }
67 }
68
69 fn increment(&mut self, hash: u64) {
71 for row in 0..SKETCH_DEPTH {
72 let col = self.index(hash, row);
73 self.table[row][col] = self.table[row][col].saturating_add(1);
74 }
75 self.additions += 1;
76 if self.additions >= self.reset_interval {
77 self.reset();
78 }
79 }
80
81 fn estimate(&self, hash: u64) -> u16 {
83 let mut min = u16::MAX;
84 for row in 0..SKETCH_DEPTH {
85 let col = self.index(hash, row);
86 min = min.min(self.table[row][col]);
87 }
88 min
89 }
90
91 fn reset(&mut self) {
93 for row in &mut self.table {
94 for cell in row.iter_mut() {
95 *cell >>= 1;
96 }
97 }
98 self.additions = 0;
99 }
100
101 fn index(&self, hash: u64, row: usize) -> usize {
104 let mixed = hash.wrapping_mul(0x9E37_79B9_7F4A_7C15_u64.wrapping_add(row as u64));
105 (mixed >> 48) as usize & (self.width - 1)
106 }
107}
108
109fn key_hash(key: &SegmentCacheKey) -> u64 {
111 use std::hash::{Hash, Hasher};
112 let mut h = std::collections::hash_map::DefaultHasher::new();
113 key.hash(&mut h);
114 h.finish()
115}
116
117#[derive(Debug, Clone)]
123pub struct CacheLoadError(pub String);
124
125impl fmt::Display for CacheLoadError {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "cache load failed: {}", self.0)
128 }
129}
130
131impl std::error::Error for CacheLoadError {}
132
133impl From<CacheLoadError> for String {
134 fn from(e: CacheLoadError) -> Self {
135 e.0
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub struct SegmentCacheKey {
145 pub segment_id: SegmentId,
147 pub row_group: u32,
149 pub column: Arc<str>,
151}
152
153struct CacheEntry {
155 array: ArrayRef,
157 byte_size: usize,
159 prev: Option<SegmentCacheKey>,
161 next: Option<SegmentCacheKey>,
163}
164
165struct InflightEntry {
170 result: Mutex<Option<Result<ArrayRef, String>>>,
172 notify: Notify,
174}
175
176struct CacheInner {
178 entries: HashMap<SegmentCacheKey, CacheEntry>,
180 lru_head: Option<SegmentCacheKey>,
182 lru_tail: Option<SegmentCacheKey>,
184 inflight: HashMap<SegmentCacheKey, Arc<InflightEntry>>,
186}
187
188pub struct SegmentCache {
200 inner: RwLock<CacheInner>,
202 sketch: Mutex<FrequencySketch>,
205 max_bytes: usize,
207 current_bytes: AtomicU64,
209 hits: AtomicU64,
211 misses: AtomicU64,
213}
214
215impl SegmentCache {
216 #[must_use]
218 pub fn new(max_bytes: usize) -> Self {
219 let estimated_entries = (max_bytes / (64 * 1024)).max(1);
222 let sketch_width = (estimated_entries * 10).clamp(DEFAULT_SKETCH_WIDTH, 1 << 20);
223 Self {
224 inner: RwLock::new(CacheInner {
225 entries: HashMap::new(),
226 lru_head: None,
227 lru_tail: None,
228 inflight: HashMap::new(),
229 }),
230 sketch: Mutex::new(FrequencySketch::with_width(sketch_width)),
231 max_bytes,
232 current_bytes: AtomicU64::new(0),
233 hits: AtomicU64::new(0),
234 misses: AtomicU64::new(0),
235 }
236 }
237
238 fn promote(inner: &mut CacheInner, key: &SegmentCacheKey) {
240 if inner.lru_head.as_ref() == Some(key) {
242 return;
243 }
244 Self::detach(inner, key);
246 Self::push_head(inner, key);
248 }
249
250 fn detach(inner: &mut CacheInner, key: &SegmentCacheKey) {
252 let (prev, next) = {
253 let entry = match inner.entries.get(key) {
254 Some(e) => e,
255 None => return,
256 };
257 (entry.prev.clone(), entry.next.clone())
258 };
259
260 if let Some(ref prev_key) = prev {
261 if let Some(prev_entry) = inner.entries.get_mut(prev_key) {
262 prev_entry.next = next.clone();
263 }
264 } else {
265 inner.lru_head = next.clone();
267 }
268
269 if let Some(ref next_key) = next {
270 if let Some(next_entry) = inner.entries.get_mut(next_key) {
271 next_entry.prev = prev;
272 }
273 } else {
274 inner.lru_tail = prev;
276 }
277
278 if let Some(entry) = inner.entries.get_mut(key) {
279 entry.prev = None;
280 entry.next = None;
281 }
282 }
283
284 fn push_head(inner: &mut CacheInner, key: &SegmentCacheKey) {
287 let old_head = inner.lru_head.take();
288 if let Some(ref old_head_key) = old_head
289 && let Some(old_entry) = inner.entries.get_mut(old_head_key)
290 {
291 old_entry.prev = Some(key.clone());
292 }
293 if let Some(entry) = inner.entries.get_mut(key) {
294 entry.prev = None;
295 entry.next = old_head.clone();
296 }
297 inner.lru_head = Some(key.clone());
298 if inner.lru_tail.is_none() {
299 inner.lru_tail = Some(key.clone());
300 }
301 }
302
303 pub async fn get_or_load<F, E>(
319 &self,
320 key: SegmentCacheKey,
321 loader: F,
322 ) -> std::result::Result<ArrayRef, E>
323 where
324 F: FnOnce() -> std::result::Result<ArrayRef, E>,
325 E: std::fmt::Display + Clone + From<CacheLoadError>,
326 {
327 enum Action {
330 Hit(ArrayRef),
331 Wait(Arc<InflightEntry>),
332 Load(Arc<InflightEntry>),
333 }
334
335 {
338 let inner = self.inner.read();
339 if let Some(entry) = inner.entries.get(&key) {
340 let arr = entry.array.clone();
341 drop(inner);
342 self.sketch.lock().increment(key_hash(&key));
343 let n = self.hits.fetch_add(1, Ordering::Relaxed);
344 if n.is_multiple_of(16) {
348 let mut w = self.inner.write();
349 Self::promote(&mut w, &key);
350 }
351 return Ok(arr);
352 }
353 }
354
355 let action = {
356 let mut inner = self.inner.write();
357
358 if let Some(entry) = inner.entries.get(&key) {
360 let arr = entry.array.clone();
361 Self::promote(&mut inner, &key);
362 self.hits.fetch_add(1, Ordering::Relaxed);
363 Action::Hit(arr)
364 } else if let Some(waiter) = inner.inflight.get(&key).cloned() {
365 self.misses.fetch_add(1, Ordering::Relaxed);
366 Action::Wait(waiter)
367 } else {
368 let entry = Arc::new(InflightEntry {
369 result: Mutex::new(None),
370 notify: Notify::new(),
371 });
372 inner.inflight.insert(key.clone(), entry.clone());
373 self.misses.fetch_add(1, Ordering::Relaxed);
374 Action::Load(entry)
375 }
376 };
378
379 match action {
380 Action::Hit(arr) => Ok(arr),
381 Action::Wait(waiter) => self.wait_inflight(&waiter).await.map_err(E::from),
382 Action::Load(entry) => {
383 let load_result = loader();
385
386 let array = match load_result {
388 Ok(arr) => {
389 *entry.result.lock() = Some(Ok(arr.clone()));
390 entry.notify.notify_waiters();
391 arr
392 }
393 Err(e) => {
394 *entry.result.lock() = Some(Err(e.to_string()));
395 entry.notify.notify_waiters();
396 self.inner.write().inflight.remove(&key);
397 return Err(e);
398 }
399 };
400
401 let byte_size = array.get_array_memory_size();
402
403 {
407 let mut inner = self.inner.write();
408 let new_hash = key_hash(&key);
409 let mut sketch = self.sketch.lock();
410 sketch.increment(new_hash);
411
412 let admit = if byte_size > self.max_bytes {
417 false } else if self.current_bytes.load(Ordering::Relaxed) as usize + byte_size
419 > self.max_bytes
420 {
421 if let Some(ref victim_key) = inner.lru_tail {
423 let victim_freq = sketch.estimate(key_hash(victim_key));
424 let new_freq = sketch.estimate(new_hash);
425 new_freq >= victim_freq
426 } else {
427 true }
429 } else {
430 true };
432 drop(sketch);
433
434 if admit {
435 self.evict_to_fit_locked(&mut inner, byte_size);
436 inner.entries.insert(
437 key.clone(),
438 CacheEntry {
439 array: array.clone(),
440 byte_size,
441 prev: None,
442 next: None,
443 },
444 );
445 Self::push_head(&mut inner, &key);
446 self.current_bytes
447 .fetch_add(byte_size as u64, Ordering::Relaxed);
448 }
449 inner.inflight.remove(&key);
450 }
451
452 Ok(array)
453 }
454 }
455 }
456
457 async fn wait_inflight(
462 &self,
463 entry: &Arc<InflightEntry>,
464 ) -> std::result::Result<ArrayRef, CacheLoadError> {
465 loop {
466 let notified = entry.notify.notified();
471
472 if let Some(ref result) = *entry.result.lock() {
474 return match result {
475 Ok(arr) => Ok(arr.clone()),
476 Err(msg) => Err(CacheLoadError(msg.clone())),
477 };
478 }
479 notified.await;
482 }
483 }
484
485 fn evict_to_fit_locked(&self, inner: &mut CacheInner, needed_bytes: usize) {
488 let target = self.max_bytes.saturating_sub(needed_bytes);
489
490 loop {
491 #[allow(clippy::cast_possible_truncation)]
492 let current = self.current_bytes.load(Ordering::Relaxed) as usize;
493 if current <= target {
494 break;
495 }
496
497 let tail_key = match inner.lru_tail.clone() {
499 Some(k) => k,
500 None => break,
501 };
502 Self::detach(inner, &tail_key);
503 if let Some(removed) = inner.entries.remove(&tail_key) {
504 self.current_bytes
505 .fetch_sub(removed.byte_size as u64, Ordering::Relaxed);
506 }
507 }
508 }
509
510 pub fn invalidate_segment(&self, segment_id: SegmentId) {
512 let mut inner = self.inner.write();
513 let keys_to_remove: Vec<SegmentCacheKey> = inner
514 .entries
515 .keys()
516 .filter(|k| k.segment_id == segment_id)
517 .cloned()
518 .collect();
519
520 for key in keys_to_remove {
521 Self::detach(&mut inner, &key);
522 if let Some(removed) = inner.entries.remove(&key) {
523 self.current_bytes
524 .fetch_sub(removed.byte_size as u64, Ordering::Relaxed);
525 }
526 }
527 }
528
529 pub fn clear(&self) {
531 let mut inner = self.inner.write();
532 inner.entries.clear();
533 inner.lru_head = None;
534 inner.lru_tail = None;
535 self.current_bytes.store(0, Ordering::Relaxed);
536 }
537
538 #[must_use]
540 pub fn size_bytes(&self) -> u64 {
541 self.current_bytes.load(Ordering::Relaxed)
542 }
543
544 #[must_use]
546 pub fn entry_count(&self) -> usize {
547 self.inner.read().entries.len()
548 }
549
550 #[must_use]
552 pub fn hits(&self) -> u64 {
553 self.hits.load(Ordering::Relaxed)
554 }
555
556 #[must_use]
558 pub fn misses(&self) -> u64 {
559 self.misses.load(Ordering::Relaxed)
560 }
561
562 #[must_use]
564 pub fn max_bytes(&self) -> usize {
565 self.max_bytes
566 }
567}
568
569impl std::fmt::Debug for SegmentCache {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 f.debug_struct("SegmentCache")
572 .field("max_bytes", &self.max_bytes)
573 .field("current_bytes", &self.current_bytes.load(Ordering::Relaxed))
574 .field("entries", &self.inner.read().entries.len())
575 .field("hits", &self.hits.load(Ordering::Relaxed))
576 .field("misses", &self.misses.load(Ordering::Relaxed))
577 .finish_non_exhaustive()
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use arrow::array::Int64Array;
585 use std::sync::Arc;
586
587 fn make_key(seg_id: u64, rg: u32, col: &str) -> SegmentCacheKey {
588 SegmentCacheKey {
589 segment_id: SegmentId(seg_id),
590 row_group: rg,
591 column: Arc::from(col),
592 }
593 }
594
595 fn make_array(size: usize) -> ArrayRef {
596 let values: Vec<i64> = (0..size as i64).collect();
597 Arc::new(Int64Array::from(values))
598 }
599
600 #[tokio::test]
601 async fn cache_hit_returns_same_data() {
602 let cache = SegmentCache::new(1024 * 1024);
603 let key = make_key(1, 0, chronix_core::TIME_COLUMN);
604 let arr = make_array(100);
605
606 let loaded = cache
607 .get_or_load(key.clone(), || Ok::<_, String>(arr.clone()))
608 .await
609 .unwrap();
610 assert_eq!(loaded.len(), 100);
611 assert_eq!(cache.misses(), 1);
612
613 let hit = cache
615 .get_or_load(key, || Ok::<_, String>(make_array(999)))
616 .await
617 .unwrap();
618 assert_eq!(hit.len(), 100); assert_eq!(cache.hits(), 1);
620 }
621
622 #[tokio::test]
626 async fn never_exceeds_byte_budget() {
627 let entry_size = make_array(64).get_array_memory_size();
628 let max_bytes = entry_size * 4;
629 let cache = SegmentCache::new(max_bytes);
630
631 for i in 0..200u64 {
632 let arr = make_array(64);
633 cache
634 .get_or_load(make_key(i, 0, "v"), || Ok::<_, String>(arr))
635 .await
636 .unwrap();
637 assert!(
638 cache.size_bytes() as usize <= max_bytes,
639 "cache overshot its budget after insert {i}: {} > {max_bytes}",
640 cache.size_bytes()
641 );
642 }
643 }
644
645 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
647 async fn concurrent_loads_respect_byte_budget() {
648 let entry_size = make_array(64).get_array_memory_size();
649 let max_bytes = entry_size * 8;
650 let cache = Arc::new(SegmentCache::new(max_bytes));
651
652 let mut handles = Vec::new();
653 for t in 0..8u64 {
654 let cache = Arc::clone(&cache);
655 handles.push(tokio::spawn(async move {
656 for i in 0..50u64 {
657 let arr = make_array(64);
658 let _ = cache
659 .get_or_load(make_key(t * 1000 + i, 0, "v"), || Ok::<_, String>(arr))
660 .await;
661 }
662 }));
663 }
664 for h in handles {
665 h.await.unwrap();
666 }
667
668 assert!(
669 cache.size_bytes() as usize <= max_bytes,
670 "cache overshot its budget under concurrency: {} > {max_bytes}",
671 cache.size_bytes()
672 );
673 }
674
675 #[tokio::test]
676 async fn eviction_removes_lru_entry() {
677 let small_arr_size = make_array(10).get_array_memory_size();
679 let cache = SegmentCache::new(small_arr_size * 2 + 10);
680
681 cache
682 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
683 .await
684 .unwrap();
685 cache
686 .get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
687 .await
688 .unwrap();
689 assert_eq!(cache.entry_count(), 2);
690
691 cache
693 .get_or_load(make_key(3, 0, "ts"), || Ok::<_, String>(make_array(10)))
694 .await
695 .unwrap();
696 assert!(cache.entry_count() <= 2);
697 }
698
699 #[tokio::test]
700 async fn invalidate_segment_clears_matching() {
701 let cache = SegmentCache::new(1024 * 1024);
702
703 cache
704 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
705 .await
706 .unwrap();
707 cache
708 .get_or_load(make_key(1, 1, "ts"), || Ok::<_, String>(make_array(10)))
709 .await
710 .unwrap();
711 cache
712 .get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
713 .await
714 .unwrap();
715 assert_eq!(cache.entry_count(), 3);
716
717 cache.invalidate_segment(SegmentId(1));
718 assert_eq!(cache.entry_count(), 1);
719 }
720
721 #[tokio::test]
722 async fn clear_empties_cache() {
723 let cache = SegmentCache::new(1024 * 1024);
724 cache
725 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
726 .await
727 .unwrap();
728 cache.clear();
729 assert_eq!(cache.entry_count(), 0);
730 assert_eq!(cache.size_bytes(), 0);
731 }
732
733 #[tokio::test]
734 async fn lru_order_evicts_oldest() {
735 let arr_size = make_array(10).get_array_memory_size();
737 let cache = SegmentCache::new(arr_size * 2 + 10);
738
739 cache
741 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
742 .await
743 .unwrap();
744 cache
745 .get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
746 .await
747 .unwrap();
748
749 cache
751 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(999)))
752 .await
753 .unwrap();
754 assert_eq!(cache.hits(), 1); cache
758 .get_or_load(make_key(3, 0, "ts"), || Ok::<_, String>(make_array(10)))
759 .await
760 .unwrap();
761
762 let r = cache
764 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(999)))
765 .await
766 .unwrap();
767 assert_eq!(cache.hits(), 2);
768 assert_eq!(r.len(), 10); let r2 = cache
772 .get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(5)))
773 .await
774 .unwrap();
775 assert_eq!(r2.len(), 5); assert_eq!(cache.misses(), 4); }
778
779 #[tokio::test]
780 async fn singleflight_dedup() {
781 use std::sync::atomic::AtomicUsize;
782
783 let cache = Arc::new(SegmentCache::new(1024 * 1024));
784 let load_count = Arc::new(AtomicUsize::new(0));
785
786 let mut handles = Vec::new();
787 for _ in 0..10 {
788 let cache = cache.clone();
789 let load_count = load_count.clone();
790 handles.push(tokio::spawn(async move {
791 cache
792 .get_or_load(make_key(42, 0, "val"), || {
793 load_count.fetch_add(1, Ordering::SeqCst);
794 std::thread::sleep(std::time::Duration::from_millis(50));
796 Ok::<_, String>(make_array(100))
797 })
798 .await
799 .unwrap()
800 }));
801 }
802
803 for h in handles {
804 let arr = h.await.unwrap();
805 assert_eq!(arr.len(), 100);
806 }
807
808 assert_eq!(load_count.load(Ordering::SeqCst), 1);
810 assert_eq!(cache.entry_count(), 1);
811 }
812
813 #[tokio::test]
814 async fn tinylfu_rejects_cold_scan() {
815 let arr_size = make_array(10).get_array_memory_size();
819 let cache = SegmentCache::new(arr_size * 2 + 10);
820
821 let key_a = make_key(1, 0, "ts");
822 let key_b = make_key(2, 0, "ts");
823
824 for _ in 0..5 {
826 cache
827 .get_or_load(key_a.clone(), || Ok::<_, String>(make_array(10)))
828 .await
829 .unwrap();
830 cache
831 .get_or_load(key_b.clone(), || Ok::<_, String>(make_array(10)))
832 .await
833 .unwrap();
834 }
835
836 for i in 100..120 {
838 cache
839 .get_or_load(make_key(i, 0, "ts"), || Ok::<_, String>(make_array(10)))
840 .await
841 .unwrap();
842 }
843
844 let r_a = cache
847 .get_or_load(key_a.clone(), || Ok::<_, String>(make_array(999)))
848 .await
849 .unwrap();
850 assert_eq!(r_a.len(), 10, "hot key A should still be cached");
851
852 let r_b = cache
853 .get_or_load(key_b.clone(), || Ok::<_, String>(make_array(999)))
854 .await
855 .unwrap();
856 assert_eq!(r_b.len(), 10, "hot key B should still be cached");
857 }
858
859 #[tokio::test]
860 async fn tinylfu_admits_hot_newcomer() {
861 let arr_size = make_array(10).get_array_memory_size();
863 let cache = SegmentCache::new(arr_size * 2 + 10);
864
865 cache
867 .get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
868 .await
869 .unwrap();
870 cache
871 .get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
872 .await
873 .unwrap();
874 assert_eq!(cache.entry_count(), 2);
875
876 let hot_key = make_key(99, 0, "ts");
880 for _ in 0..5 {
881 cache
882 .get_or_load(hot_key.clone(), || Ok::<_, String>(make_array(10)))
883 .await
884 .unwrap();
885 }
886
887 let inner = cache.inner.read();
889 assert!(
890 inner.entries.contains_key(&hot_key),
891 "hot newcomer should have been admitted"
892 );
893 }
894
895 #[tokio::test]
896 async fn sketch_reset_prevents_count_bloat() {
897 let mut sketch = FrequencySketch::new();
898 let reset_interval = sketch.reset_interval;
899
900 let key_hash = 42u64;
902 for _ in 0..(reset_interval * 3) {
903 sketch.increment(key_hash);
904 }
905
906 let est = sketch.estimate(key_hash);
909 assert!(
910 est < 25000,
911 "sketch estimate should be bounded after resets, got {est}"
912 );
913 }
914}