use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use crate::segment::{ColumnMeta, SegmentHeader};
use chronix_core::SegmentId;
#[derive(Debug, Clone)]
pub struct CachedSegmentMeta {
pub segment_id: SegmentId,
pub header: SegmentHeader,
pub columns: Vec<ColumnMeta>,
}
pub struct MetadataCache {
entries: Arc<RwLock<HashMap<SegmentId, Arc<CachedSegmentMeta>>>>,
max_entries: usize,
}
const DEFAULT_MAX_ENTRIES: usize = 500_000;
impl MetadataCache {
#[must_use]
pub fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
max_entries: DEFAULT_MAX_ENTRIES,
}
}
#[must_use]
pub fn with_max_entries(max_entries: usize) -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
max_entries,
}
}
pub fn insert(&self, meta: CachedSegmentMeta) {
let mut map = self.entries.write();
map.insert(meta.segment_id, Arc::new(meta));
if self.max_entries > 0 && map.len() > self.max_entries {
tracing::warn!(
cache_len = map.len(),
max_entries = self.max_entries,
"MetadataCache exceeded max_entries — consider raising the limit or investigating segment count",
);
}
}
#[must_use]
pub fn get(&self, segment_id: SegmentId) -> Option<Arc<CachedSegmentMeta>> {
self.entries.read().get(&segment_id).cloned()
}
pub fn remove(&self, segment_id: SegmentId) {
self.entries.write().remove(&segment_id);
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
pub fn clear(&self) {
self.entries.write().clear();
}
#[must_use]
pub fn columns_for(&self, segment_id: SegmentId) -> Option<Vec<ColumnMeta>> {
self.entries
.read()
.get(&segment_id)
.map(|m| m.columns.clone())
}
}
impl Default for MetadataCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MetadataCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetadataCache")
.field("entries", &self.entries.read().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::segment::ColumnStats;
fn make_header() -> SegmentHeader {
SegmentHeader {
version: 1,
flags: 0,
created_at: 0,
min_timestamp: 100,
max_timestamp: 200,
row_count: 1000,
column_count: 3,
series_count: 5,
compression: 0,
sort_order: 1,
}
}
fn make_column_meta(name: &str) -> ColumnMeta {
ColumnMeta {
name: name.to_string(),
data_type: 2,
role: 2,
default_encoding: 1,
stats: ColumnStats::empty(),
bloom_filter: None,
row_group_blooms: None,
encrypted: false,
key_id: None,
}
}
#[test]
fn insert_and_get() {
let cache = MetadataCache::new();
cache.insert(CachedSegmentMeta {
segment_id: SegmentId(1),
header: make_header(),
columns: vec![make_column_meta("cpu")],
});
let result = cache.get(SegmentId(1));
assert!(result.is_some());
assert_eq!(result.unwrap().columns.len(), 1);
}
#[test]
fn get_nonexistent_returns_none() {
let cache = MetadataCache::new();
assert!(cache.get(SegmentId(99)).is_none());
}
#[test]
fn remove_entry() {
let cache = MetadataCache::new();
cache.insert(CachedSegmentMeta {
segment_id: SegmentId(1),
header: make_header(),
columns: Vec::new(),
});
assert_eq!(cache.len(), 1);
cache.remove(SegmentId(1));
assert_eq!(cache.len(), 0);
}
#[test]
fn columns_for_segment() {
let cache = MetadataCache::new();
cache.insert(CachedSegmentMeta {
segment_id: SegmentId(1),
header: make_header(),
columns: vec![make_column_meta("cpu"), make_column_meta("mem")],
});
let cols = cache.columns_for(SegmentId(1)).unwrap();
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].name, "cpu");
}
}