use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::error::{DbError, Result};
use metrics::gauge;
use chronix_core::{ChronixConfig, MeasurementSchema, SchemaRegistry};
use chronix_engine::index::{SegmentCatalog, SeriesBloomFilter, TagInvertedIndex};
use chronix_engine::memtable::ShardRouter;
use chronix_engine::wal::WalWriter;
use chronix_streaming::cdc::{EventBus, FilteredSubscription, SubscriptionFilter};
use crate::lock_order::{BloomsLock, CatalogLock, RollupRegistryLock};
use crate::rollup::RollupRegistry;
use super::{Chronix, DatabaseStatistics};
impl Chronix {
#[cfg(feature = "sql")]
pub fn register_udf(&self, udf: Arc<datafusion::logical_expr::ScalarUDF>) {
*self.sql_ctx.write() = None;
self.custom_udfs.write().push(udf);
}
#[cfg(feature = "sql")]
pub fn register_udaf(&self, udaf: Arc<datafusion::logical_expr::AggregateUDF>) {
*self.sql_ctx.write() = None;
self.custom_udafs.write().push(udaf);
}
#[cfg(feature = "sql")]
pub fn sql(&self, query: &str) -> Result<Vec<arrow::record_batch::RecordBatch>> {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread {
return tokio::task::block_in_place(|| handle.block_on(self.sql_async(query)));
}
return Err(DbError::Internal(
"Chronix::sql() blocks and was called inside a current-thread async runtime; \
use sql_async()"
.into(),
));
}
let runtime = self.sql_runtime.get_or_init(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a current-thread tokio runtime can always be built")
});
runtime.block_on(self.sql_async(query))
}
#[cfg(feature = "sql")]
pub async fn sql_async(&self, query: &str) -> Result<Vec<arrow::record_batch::RecordBatch>> {
let ctx = self.session_context();
let df = crate::sql::sql_read_only(&ctx, query).await?;
Ok(df.collect().await?)
}
#[cfg(feature = "sql")]
pub fn session_context(&self) -> datafusion::execution::context::SessionContext {
if let Some(ctx) = self.sql_ctx.read().as_ref() {
return ctx.clone();
}
let mut slot = self.sql_ctx.write();
if let Some(ctx) = slot.as_ref() {
return ctx.clone();
}
let ctx = crate::sql::create_session_context(Arc::new(self.clone()));
*slot = Some(ctx.clone());
ctx
}
pub fn promql(&self, query: &str, at_ns: i64) -> Result<crate::promql::ast::PromQLValue> {
let expr = crate::promql::parse(query).map_err(|e| DbError::PromQl(e.to_string()))?;
let params = crate::promql::eval::QueryParams {
time: at_ns,
..Default::default()
};
crate::promql::PromQLEvaluator::new(Arc::new(self.clone()))
.instant_query(&expr, ¶ms)
.map_err(|e| DbError::PromQl(e.0))
}
pub fn promql_range(
&self,
query: &str,
start_ns: i64,
end_ns: i64,
step_ns: i64,
) -> Result<crate::promql::ast::PromQLValue> {
let expr = crate::promql::parse(query).map_err(|e| DbError::PromQl(e.to_string()))?;
let params = crate::promql::eval::QueryParams {
time: end_ns,
start: Some(start_ns),
end: Some(end_ns),
step: Some(step_ns),
..Default::default()
};
crate::promql::PromQLEvaluator::new(Arc::new(self.clone()))
.range_query(&expr, ¶ms)
.map_err(|e| DbError::PromQl(e.0))
}
#[cfg(feature = "sql")]
pub fn custom_udfs(&self) -> Vec<Arc<datafusion::logical_expr::ScalarUDF>> {
self.custom_udfs.read().clone()
}
#[cfg(feature = "sql")]
pub fn custom_udafs(&self) -> Vec<Arc<datafusion::logical_expr::AggregateUDF>> {
self.custom_udafs.read().clone()
}
#[must_use]
pub fn schema(&self, measurement: &str) -> Option<Arc<MeasurementSchema>> {
if self.catalog.read().is_measurement_pending_drop(measurement) {
return None;
}
self.schema.lookup(measurement)
}
#[must_use]
pub fn schema_registry(&self) -> &SchemaRegistry {
&self.schema
}
pub fn catalog(&self) -> &Arc<CatalogLock<SegmentCatalog>> {
&self.catalog
}
#[must_use]
pub fn shards(&self) -> &Arc<ShardRouter> {
&self.shards
}
#[must_use]
pub fn config(&self) -> &ChronixConfig {
&self.config
}
#[must_use]
pub fn statistics(&self) -> DatabaseStatistics {
let series_count = self.known_series.len();
let segment_count = {
let catalog = self.catalog.read();
catalog.segment_count()
};
let shard_count = {
let ti = self.time_index.read();
ti.len()
};
let memtable_memory_bytes = self.shards.total_memory();
let interner_memory_bytes = self.shards.total_interner_memory();
let wal_buffer_bytes = self.wal.memory_bytes();
let catalog_memory_bytes = self.catalog.read().memory_bytes();
let measurement_count = self.schema.measurement_count();
let wal_sequence = self.wal.current_sequence();
let tombstone_count = {
let ts = self.tombstones.read();
ts.len()
};
let metadata_cache_entries = self.metadata_cache.len();
let metadata_cache_bytes = self.metadata_cache.memory_bytes();
gauge!("chronix_series_count").set(series_count as f64);
gauge!("chronix_segment_count").set(segment_count as f64);
gauge!("chronix_shard_count").set(shard_count as f64);
gauge!("chronix_memtable_memory_bytes").set(memtable_memory_bytes as f64);
gauge!("chronix_interner_memory_bytes").set(interner_memory_bytes as f64);
gauge!("chronix_wal_buffer_bytes").set(wal_buffer_bytes as f64);
gauge!("chronix_catalog_memory_bytes").set(catalog_memory_bytes as f64);
gauge!("chronix_measurement_count").set(measurement_count as f64);
gauge!("chronix_wal_sequence").set(wal_sequence as f64);
gauge!("chronix_tombstone_count").set(tombstone_count as f64);
gauge!("chronix_metadata_cache_entries").set(metadata_cache_entries as f64);
gauge!("chronix_metadata_cache_bytes").set(metadata_cache_bytes as f64);
gauge!("chronix_storage_disk_usage_bytes").set(self.disk_usage_bytes() as f64);
DatabaseStatistics {
series_count,
segment_count,
shard_count,
memtable_memory_bytes,
interner_memory_bytes,
wal_buffer_bytes,
catalog_memory_bytes,
measurement_count,
wal_sequence,
tombstone_count,
metadata_cache_entries,
metadata_cache_bytes,
}
}
#[must_use]
pub fn disk_usage_bytes(&self) -> u64 {
const TTL: Duration = Duration::from_secs(60);
{
let cached = self.disk_usage.read();
if let Some((measured_at, bytes)) = *cached {
if measured_at.elapsed() < TTL {
return bytes;
}
}
}
let bytes = directory_size(self.data_dir());
*self.disk_usage.write() = Some((Instant::now(), bytes));
bytes
}
#[must_use]
pub fn measurement_names_in(&self, namespace: Option<&str>) -> Vec<String> {
let Some(namespace) = namespace else {
let pending = self.catalog.read().pending_measurement_drops().clone();
return self
.schema
.measurement_names()
.into_iter()
.filter(|m| !pending.contains_key(m))
.collect();
};
let mut names: Vec<String> = self
.namespace_measurements
.get(namespace)
.map(|set| set.iter().map(|m| m.clone()).collect())
.unwrap_or_default();
names.sort_unstable();
names
}
#[must_use]
pub fn has_measurement_in(&self, namespace: Option<&str>, measurement: &str) -> bool {
match namespace {
None => self.schema(measurement).is_some(),
Some(namespace) => self
.namespace_measurements
.get(namespace)
.is_some_and(|set| set.contains(measurement)),
}
}
#[must_use]
pub fn wal(&self) -> &Arc<WalWriter> {
&self.wal
}
#[must_use]
pub fn data_dir(&self) -> &Path {
&self.config.data_dir
}
#[must_use]
pub fn wal_replayed_records(&self) -> usize {
self.replayed_records
}
#[must_use]
pub fn wal_sequence(&self) -> u64 {
self.wal.current_sequence()
}
pub fn bloom_filters(&self) -> &Arc<BloomsLock<BTreeMap<u64, SeriesBloomFilter>>> {
&self.blooms
}
pub fn tag_index(&self) -> &Arc<TagInvertedIndex> {
&self.tag_index
}
#[must_use]
pub fn tag_keys(&self) -> Vec<String> {
self.tag_index.all_keys()
}
#[must_use]
pub fn tag_values(&self, key: &str) -> Vec<String> {
self.tag_index.values_for_key(key)
}
pub fn event_bus(&self) -> &EventBus {
&self.cdc_bus
}
pub fn subscribe(&self, filter: SubscriptionFilter) -> FilteredSubscription {
FilteredSubscription::new(&self.cdc_bus, filter)
}
pub fn rollup_registry(&self) -> &Arc<RollupRegistryLock<RollupRegistry>> {
&self.rollup_registry
}
}
fn directory_size(root: &Path) -> u64 {
let mut total = 0u64;
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let Ok(meta) = entry.metadata() else { continue };
if meta.is_dir() {
stack.push(entry.path());
} else if meta.is_file() {
total += meta.len();
}
}
}
total
}
pub(super) fn index_by_namespace(
known_series: &dashmap::DashSet<String>,
) -> dashmap::DashMap<String, dashmap::DashSet<String>> {
let index = dashmap::DashMap::new();
for key in known_series.iter() {
record_series(&index, key.as_str());
}
index
}
pub(super) fn record_series(
index: &dashmap::DashMap<String, dashmap::DashSet<String>>,
canonical: &str,
) {
let Some((measurement, namespace)) = split_canonical(canonical) else {
return;
};
index
.entry(namespace.to_string())
.or_default()
.insert(measurement.to_string());
}
pub(super) fn split_canonical(canonical: &str) -> Option<(&str, &str)> {
let mut parts = canonical.split(chronix_core::TAG_SEPARATOR);
let measurement = parts.next()?;
let namespace = parts
.find_map(|tag| {
let (key, value) = tag.split_once(chronix_core::KV_SEPARATOR)?;
(key == chronix_core::NAMESPACE_TAG).then_some(value)
})
.unwrap_or("");
Some((measurement, namespace))
}