use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use arrow::array::ArrayRef;
use parking_lot::{Mutex, RwLock};
use tokio::sync::Notify;
use chronix_core::SegmentId;
const SKETCH_DEPTH: usize = 4;
const DEFAULT_SKETCH_WIDTH: usize = 2048;
struct FrequencySketch {
table: Vec<Vec<u16>>,
width: usize,
additions: u64,
reset_interval: u64,
}
impl FrequencySketch {
#[cfg(test)]
fn new() -> Self {
Self::with_width(DEFAULT_SKETCH_WIDTH)
}
fn with_width(width: usize) -> Self {
let width = width.next_power_of_two().max(64);
Self {
table: vec![vec![0u16; width]; SKETCH_DEPTH],
width,
additions: 0,
reset_interval: 10 * width as u64,
}
}
fn increment(&mut self, hash: u64) {
for row in 0..SKETCH_DEPTH {
let col = self.index(hash, row);
self.table[row][col] = self.table[row][col].saturating_add(1);
}
self.additions += 1;
if self.additions >= self.reset_interval {
self.reset();
}
}
fn estimate(&self, hash: u64) -> u16 {
let mut min = u16::MAX;
for row in 0..SKETCH_DEPTH {
let col = self.index(hash, row);
min = min.min(self.table[row][col]);
}
min
}
fn reset(&mut self) {
for row in &mut self.table {
for cell in row.iter_mut() {
*cell >>= 1;
}
}
self.additions = 0;
}
fn index(&self, hash: u64, row: usize) -> usize {
let mixed = hash.wrapping_mul(0x9E37_79B9_7F4A_7C15_u64.wrapping_add(row as u64));
(mixed >> 48) as usize & (self.width - 1)
}
}
fn key_hash(key: &SegmentCacheKey) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut h);
h.finish()
}
#[derive(Debug, Clone)]
pub struct CacheLoadError(pub String);
impl fmt::Display for CacheLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cache load failed: {}", self.0)
}
}
impl std::error::Error for CacheLoadError {}
impl From<CacheLoadError> for String {
fn from(e: CacheLoadError) -> Self {
e.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SegmentCacheKey {
pub segment_id: SegmentId,
pub row_group: u32,
pub column: Arc<str>,
}
struct CacheEntry {
array: ArrayRef,
byte_size: usize,
prev: Option<SegmentCacheKey>,
next: Option<SegmentCacheKey>,
}
struct InflightEntry {
result: Mutex<Option<Result<ArrayRef, String>>>,
notify: Notify,
}
struct CacheInner {
entries: HashMap<SegmentCacheKey, CacheEntry>,
lru_head: Option<SegmentCacheKey>,
lru_tail: Option<SegmentCacheKey>,
inflight: HashMap<SegmentCacheKey, Arc<InflightEntry>>,
}
pub struct SegmentCache {
inner: RwLock<CacheInner>,
sketch: Mutex<FrequencySketch>,
max_bytes: usize,
current_bytes: AtomicU64,
hits: AtomicU64,
misses: AtomicU64,
}
impl SegmentCache {
#[must_use]
pub fn new(max_bytes: usize) -> Self {
let estimated_entries = (max_bytes / (64 * 1024)).max(1);
let sketch_width = (estimated_entries * 10).clamp(DEFAULT_SKETCH_WIDTH, 1 << 20);
Self {
inner: RwLock::new(CacheInner {
entries: HashMap::new(),
lru_head: None,
lru_tail: None,
inflight: HashMap::new(),
}),
sketch: Mutex::new(FrequencySketch::with_width(sketch_width)),
max_bytes,
current_bytes: AtomicU64::new(0),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
}
}
fn promote(inner: &mut CacheInner, key: &SegmentCacheKey) {
if inner.lru_head.as_ref() == Some(key) {
return;
}
Self::detach(inner, key);
Self::push_head(inner, key);
}
fn detach(inner: &mut CacheInner, key: &SegmentCacheKey) {
let (prev, next) = {
let entry = match inner.entries.get(key) {
Some(e) => e,
None => return,
};
(entry.prev.clone(), entry.next.clone())
};
if let Some(ref prev_key) = prev {
if let Some(prev_entry) = inner.entries.get_mut(prev_key) {
prev_entry.next = next.clone();
}
} else {
inner.lru_head = next.clone();
}
if let Some(ref next_key) = next {
if let Some(next_entry) = inner.entries.get_mut(next_key) {
next_entry.prev = prev;
}
} else {
inner.lru_tail = prev;
}
if let Some(entry) = inner.entries.get_mut(key) {
entry.prev = None;
entry.next = None;
}
}
fn push_head(inner: &mut CacheInner, key: &SegmentCacheKey) {
let old_head = inner.lru_head.take();
if let Some(ref old_head_key) = old_head
&& let Some(old_entry) = inner.entries.get_mut(old_head_key)
{
old_entry.prev = Some(key.clone());
}
if let Some(entry) = inner.entries.get_mut(key) {
entry.prev = None;
entry.next = old_head.clone();
}
inner.lru_head = Some(key.clone());
if inner.lru_tail.is_none() {
inner.lru_tail = Some(key.clone());
}
}
pub async fn get_or_load<F, E>(
&self,
key: SegmentCacheKey,
loader: F,
) -> std::result::Result<ArrayRef, E>
where
F: FnOnce() -> std::result::Result<ArrayRef, E>,
E: std::fmt::Display + Clone + From<CacheLoadError>,
{
enum Action {
Hit(ArrayRef),
Wait(Arc<InflightEntry>),
Load(Arc<InflightEntry>),
}
{
let inner = self.inner.read();
if let Some(entry) = inner.entries.get(&key) {
let arr = entry.array.clone();
drop(inner);
self.sketch.lock().increment(key_hash(&key));
let n = self.hits.fetch_add(1, Ordering::Relaxed);
if n.is_multiple_of(16) {
let mut w = self.inner.write();
Self::promote(&mut w, &key);
}
return Ok(arr);
}
}
let action = {
let mut inner = self.inner.write();
if let Some(entry) = inner.entries.get(&key) {
let arr = entry.array.clone();
Self::promote(&mut inner, &key);
self.hits.fetch_add(1, Ordering::Relaxed);
Action::Hit(arr)
} else if let Some(waiter) = inner.inflight.get(&key).cloned() {
self.misses.fetch_add(1, Ordering::Relaxed);
Action::Wait(waiter)
} else {
let entry = Arc::new(InflightEntry {
result: Mutex::new(None),
notify: Notify::new(),
});
inner.inflight.insert(key.clone(), entry.clone());
self.misses.fetch_add(1, Ordering::Relaxed);
Action::Load(entry)
}
};
match action {
Action::Hit(arr) => Ok(arr),
Action::Wait(waiter) => self.wait_inflight(&waiter).await.map_err(E::from),
Action::Load(entry) => {
let load_result = loader();
let array = match load_result {
Ok(arr) => {
*entry.result.lock() = Some(Ok(arr.clone()));
entry.notify.notify_waiters();
arr
}
Err(e) => {
*entry.result.lock() = Some(Err(e.to_string()));
entry.notify.notify_waiters();
self.inner.write().inflight.remove(&key);
return Err(e);
}
};
let byte_size = array.get_array_memory_size();
{
let mut inner = self.inner.write();
let new_hash = key_hash(&key);
let mut sketch = self.sketch.lock();
sketch.increment(new_hash);
let admit = if byte_size > self.max_bytes {
false } else if self.current_bytes.load(Ordering::Relaxed) as usize + byte_size
> self.max_bytes
{
if let Some(ref victim_key) = inner.lru_tail {
let victim_freq = sketch.estimate(key_hash(victim_key));
let new_freq = sketch.estimate(new_hash);
new_freq >= victim_freq
} else {
true }
} else {
true };
drop(sketch);
if admit {
self.evict_to_fit_locked(&mut inner, byte_size);
inner.entries.insert(
key.clone(),
CacheEntry {
array: array.clone(),
byte_size,
prev: None,
next: None,
},
);
Self::push_head(&mut inner, &key);
self.current_bytes
.fetch_add(byte_size as u64, Ordering::Relaxed);
}
inner.inflight.remove(&key);
}
Ok(array)
}
}
}
async fn wait_inflight(
&self,
entry: &Arc<InflightEntry>,
) -> std::result::Result<ArrayRef, CacheLoadError> {
loop {
let notified = entry.notify.notified();
if let Some(ref result) = *entry.result.lock() {
return match result {
Ok(arr) => Ok(arr.clone()),
Err(msg) => Err(CacheLoadError(msg.clone())),
};
}
notified.await;
}
}
fn evict_to_fit_locked(&self, inner: &mut CacheInner, needed_bytes: usize) {
let target = self.max_bytes.saturating_sub(needed_bytes);
loop {
#[allow(clippy::cast_possible_truncation)]
let current = self.current_bytes.load(Ordering::Relaxed) as usize;
if current <= target {
break;
}
let tail_key = match inner.lru_tail.clone() {
Some(k) => k,
None => break,
};
Self::detach(inner, &tail_key);
if let Some(removed) = inner.entries.remove(&tail_key) {
self.current_bytes
.fetch_sub(removed.byte_size as u64, Ordering::Relaxed);
}
}
}
pub fn invalidate_segment(&self, segment_id: SegmentId) {
let mut inner = self.inner.write();
let keys_to_remove: Vec<SegmentCacheKey> = inner
.entries
.keys()
.filter(|k| k.segment_id == segment_id)
.cloned()
.collect();
for key in keys_to_remove {
Self::detach(&mut inner, &key);
if let Some(removed) = inner.entries.remove(&key) {
self.current_bytes
.fetch_sub(removed.byte_size as u64, Ordering::Relaxed);
}
}
}
pub fn clear(&self) {
let mut inner = self.inner.write();
inner.entries.clear();
inner.lru_head = None;
inner.lru_tail = None;
self.current_bytes.store(0, Ordering::Relaxed);
}
#[must_use]
pub fn size_bytes(&self) -> u64 {
self.current_bytes.load(Ordering::Relaxed)
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.inner.read().entries.len()
}
#[must_use]
pub fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
#[must_use]
pub fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
#[must_use]
pub fn max_bytes(&self) -> usize {
self.max_bytes
}
}
impl std::fmt::Debug for SegmentCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SegmentCache")
.field("max_bytes", &self.max_bytes)
.field("current_bytes", &self.current_bytes.load(Ordering::Relaxed))
.field("entries", &self.inner.read().entries.len())
.field("hits", &self.hits.load(Ordering::Relaxed))
.field("misses", &self.misses.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::Int64Array;
use std::sync::Arc;
fn make_key(seg_id: u64, rg: u32, col: &str) -> SegmentCacheKey {
SegmentCacheKey {
segment_id: SegmentId(seg_id),
row_group: rg,
column: Arc::from(col),
}
}
fn make_array(size: usize) -> ArrayRef {
let values: Vec<i64> = (0..size as i64).collect();
Arc::new(Int64Array::from(values))
}
#[tokio::test]
async fn cache_hit_returns_same_data() {
let cache = SegmentCache::new(1024 * 1024);
let key = make_key(1, 0, chronix_core::TIME_COLUMN);
let arr = make_array(100);
let loaded = cache
.get_or_load(key.clone(), || Ok::<_, String>(arr.clone()))
.await
.unwrap();
assert_eq!(loaded.len(), 100);
assert_eq!(cache.misses(), 1);
let hit = cache
.get_or_load(key, || Ok::<_, String>(make_array(999)))
.await
.unwrap();
assert_eq!(hit.len(), 100); assert_eq!(cache.hits(), 1);
}
#[tokio::test]
async fn never_exceeds_byte_budget() {
let entry_size = make_array(64).get_array_memory_size();
let max_bytes = entry_size * 4;
let cache = SegmentCache::new(max_bytes);
for i in 0..200u64 {
let arr = make_array(64);
cache
.get_or_load(make_key(i, 0, "v"), || Ok::<_, String>(arr))
.await
.unwrap();
assert!(
cache.size_bytes() as usize <= max_bytes,
"cache overshot its budget after insert {i}: {} > {max_bytes}",
cache.size_bytes()
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_loads_respect_byte_budget() {
let entry_size = make_array(64).get_array_memory_size();
let max_bytes = entry_size * 8;
let cache = Arc::new(SegmentCache::new(max_bytes));
let mut handles = Vec::new();
for t in 0..8u64 {
let cache = Arc::clone(&cache);
handles.push(tokio::spawn(async move {
for i in 0..50u64 {
let arr = make_array(64);
let _ = cache
.get_or_load(make_key(t * 1000 + i, 0, "v"), || Ok::<_, String>(arr))
.await;
}
}));
}
for h in handles {
h.await.unwrap();
}
assert!(
cache.size_bytes() as usize <= max_bytes,
"cache overshot its budget under concurrency: {} > {max_bytes}",
cache.size_bytes()
);
}
#[tokio::test]
async fn eviction_removes_lru_entry() {
let small_arr_size = make_array(10).get_array_memory_size();
let cache = SegmentCache::new(small_arr_size * 2 + 10);
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
assert_eq!(cache.entry_count(), 2);
cache
.get_or_load(make_key(3, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
assert!(cache.entry_count() <= 2);
}
#[tokio::test]
async fn invalidate_segment_clears_matching() {
let cache = SegmentCache::new(1024 * 1024);
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(1, 1, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
assert_eq!(cache.entry_count(), 3);
cache.invalidate_segment(SegmentId(1));
assert_eq!(cache.entry_count(), 1);
}
#[tokio::test]
async fn clear_empties_cache() {
let cache = SegmentCache::new(1024 * 1024);
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache.clear();
assert_eq!(cache.entry_count(), 0);
assert_eq!(cache.size_bytes(), 0);
}
#[tokio::test]
async fn lru_order_evicts_oldest() {
let arr_size = make_array(10).get_array_memory_size();
let cache = SegmentCache::new(arr_size * 2 + 10);
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(999)))
.await
.unwrap();
assert_eq!(cache.hits(), 1);
cache
.get_or_load(make_key(3, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
let r = cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(999)))
.await
.unwrap();
assert_eq!(cache.hits(), 2);
assert_eq!(r.len(), 10);
let r2 = cache
.get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(5)))
.await
.unwrap();
assert_eq!(r2.len(), 5); assert_eq!(cache.misses(), 4); }
#[tokio::test]
async fn singleflight_dedup() {
use std::sync::atomic::AtomicUsize;
let cache = Arc::new(SegmentCache::new(1024 * 1024));
let load_count = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let cache = cache.clone();
let load_count = load_count.clone();
handles.push(tokio::spawn(async move {
cache
.get_or_load(make_key(42, 0, "val"), || {
load_count.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(50));
Ok::<_, String>(make_array(100))
})
.await
.unwrap()
}));
}
for h in handles {
let arr = h.await.unwrap();
assert_eq!(arr.len(), 100);
}
assert_eq!(load_count.load(Ordering::SeqCst), 1);
assert_eq!(cache.entry_count(), 1);
}
#[tokio::test]
async fn tinylfu_rejects_cold_scan() {
let arr_size = make_array(10).get_array_memory_size();
let cache = SegmentCache::new(arr_size * 2 + 10);
let key_a = make_key(1, 0, "ts");
let key_b = make_key(2, 0, "ts");
for _ in 0..5 {
cache
.get_or_load(key_a.clone(), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(key_b.clone(), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
}
for i in 100..120 {
cache
.get_or_load(make_key(i, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
}
let r_a = cache
.get_or_load(key_a.clone(), || Ok::<_, String>(make_array(999)))
.await
.unwrap();
assert_eq!(r_a.len(), 10, "hot key A should still be cached");
let r_b = cache
.get_or_load(key_b.clone(), || Ok::<_, String>(make_array(999)))
.await
.unwrap();
assert_eq!(r_b.len(), 10, "hot key B should still be cached");
}
#[tokio::test]
async fn tinylfu_admits_hot_newcomer() {
let arr_size = make_array(10).get_array_memory_size();
let cache = SegmentCache::new(arr_size * 2 + 10);
cache
.get_or_load(make_key(1, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
cache
.get_or_load(make_key(2, 0, "ts"), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
assert_eq!(cache.entry_count(), 2);
let hot_key = make_key(99, 0, "ts");
for _ in 0..5 {
cache
.get_or_load(hot_key.clone(), || Ok::<_, String>(make_array(10)))
.await
.unwrap();
}
let inner = cache.inner.read();
assert!(
inner.entries.contains_key(&hot_key),
"hot newcomer should have been admitted"
);
}
#[tokio::test]
async fn sketch_reset_prevents_count_bloat() {
let mut sketch = FrequencySketch::new();
let reset_interval = sketch.reset_interval;
let key_hash = 42u64;
for _ in 0..(reset_interval * 3) {
sketch.increment(key_hash);
}
let est = sketch.estimate(key_hash);
assert!(
est < 25000,
"sketch estimate should be bounded after resets, got {est}"
);
}
}