use tracing::info;
use crate::data::executor::core_loop::CoreLoop;
use crate::engine::timeseries::partition_registry::{PartitionEntry, PartitionRegistry};
use crate::types::{DatabaseId, TenantId};
impl CoreLoop {
pub fn load_ts_registries(&mut self) -> crate::Result<()> {
let ts_root = self.data_dir.join("ts");
if !ts_root.exists() {
return Ok(());
}
let mut loaded = 0usize;
let mut partitions = 0usize;
for (database_id, tenant_id, collection) in enumerate_ts_collections(&ts_root) {
self.ensure_ts_registry(tenant_id, database_id, &collection)?;
let key = (database_id, tenant_id, collection);
if let Some(reg) = self.ts_registries.get(&key) {
loaded += 1;
partitions += reg.partition_count();
}
}
if loaded > 0 {
info!(
core = self.core_id,
collections = loaded,
partitions,
"timeseries partition registries restored"
);
}
Ok(())
}
pub(in crate::data::executor) fn ensure_ts_registry(
&mut self,
tid: TenantId,
database_id: DatabaseId,
collection: &str,
) -> crate::Result<()> {
let key = (database_id, tid, collection.to_string());
if self.ts_registries.contains_key(&key) {
return Ok(());
}
let ts_dir = crate::data::executor::handlers::timeseries::paths::ts_collection_dir(
&self.data_dir,
database_id.as_u64(),
tid.as_u64(),
collection,
);
if !ts_dir.exists() {
return Ok(());
}
let registry = read_registry(&ts_dir, self.segment_keks.ts_segment_kek.as_ref())?;
if registry.partition_count() > 0 {
info!(
collection,
partitions = registry.partition_count(),
"loaded partition registry from disk"
);
}
self.ts_registries.insert(key, registry);
Ok(())
}
}
fn enumerate_ts_collections(ts_root: &std::path::Path) -> Vec<(DatabaseId, TenantId, String)> {
let mut out = Vec::new();
let Ok(db_dirs) = std::fs::read_dir(ts_root) else {
return out;
};
for db_dir in db_dirs.flatten() {
let Some(database_id) = db_dir
.file_name()
.to_str()
.and_then(|n| n.parse::<u64>().ok())
else {
continue;
};
let Ok(tenant_dirs) = std::fs::read_dir(db_dir.path()) else {
continue;
};
for tenant_dir in tenant_dirs.flatten() {
let Some(tenant_id) = tenant_dir
.file_name()
.to_str()
.and_then(|n| n.parse::<u64>().ok())
else {
continue;
};
let Ok(coll_dirs) = std::fs::read_dir(tenant_dir.path()) else {
continue;
};
for coll_dir in coll_dirs.flatten() {
if !coll_dir.path().is_dir() {
continue;
}
let Some(collection) = coll_dir.file_name().to_str().map(|s| s.to_string()) else {
continue;
};
out.push((
DatabaseId::new(database_id),
TenantId::new(tenant_id),
collection,
));
}
}
}
out
}
fn read_registry(
ts_dir: &std::path::Path,
kek: Option<&nodedb_wal::crypto::WalEncryptionKey>,
) -> crate::Result<PartitionRegistry> {
let mut registry =
PartitionRegistry::new(nodedb_types::timeseries::TieredPartitionConfig::origin_defaults());
let entries = std::fs::read_dir(ts_dir).map_err(|e| crate::Error::Storage {
engine: "timeseries".to_string(),
detail: format!("read partition dir {}: {e}", ts_dir.display()),
})?;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(dir_name) = name.to_str() else {
continue;
};
if !dir_name.starts_with("ts-") || !entry.path().is_dir() {
continue;
}
let meta_path = entry.path().join("partition.meta");
if !meta_path.exists() {
continue;
}
let meta = read_partition_meta(&meta_path, kek)?;
registry.import(vec![(
meta.min_ts,
PartitionEntry {
meta,
dir_name: dir_name.to_string(),
},
)]);
}
Ok(registry)
}
fn read_partition_meta(
path: &std::path::Path,
kek: Option<&nodedb_wal::crypto::WalEncryptionKey>,
) -> crate::Result<nodedb_types::timeseries::PartitionMeta> {
let raw =
nodedb_wal::segment::read_checkpoint_dontneed(path).map_err(|e| crate::Error::Storage {
engine: "timeseries".to_string(),
detail: format!("read {}: {e}", path.display()),
})?;
let encrypted = crate::engine::timeseries::columnar_segment::encrypt::is_encrypted(&raw)
.map_err(|e| crate::Error::Storage {
engine: "timeseries".to_string(),
detail: format!("sniff {}: {e}", path.display()),
})?;
let bytes = if encrypted {
let key = kek.ok_or_else(|| crate::Error::Storage {
engine: "timeseries".to_string(),
detail: format!(
"{} is SEGT-encrypted but no timeseries segment key is installed",
path.display()
),
})?;
crate::engine::timeseries::columnar_segment::encrypt::decrypt_file(key, &raw).map_err(
|e| crate::Error::Storage {
engine: "timeseries".to_string(),
detail: format!("decrypt {}: {e}", path.display()),
},
)?
} else {
raw
};
sonic_rs::from_slice(&bytes).map_err(|e| crate::Error::Serialization {
format: "json".to_string(),
detail: format!("parse {}: {e}", path.display()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enumerate_walks_database_tenant_collection() {
let dir = tempfile::tempdir().expect("tempdir");
let ts_root = dir.path().join("ts");
std::fs::create_dir_all(ts_root.join("0").join("1").join("metrics")).expect("mkdir");
std::fs::create_dir_all(ts_root.join("0").join("2").join("events")).expect("mkdir");
std::fs::create_dir_all(ts_root.join("not-a-db").join("1").join("x")).expect("mkdir");
let mut found = enumerate_ts_collections(&ts_root);
found.sort_by_key(|entry| (entry.1.as_u64(), entry.2.clone()));
assert_eq!(
found,
vec![
(DatabaseId::new(0), TenantId::new(1), "metrics".to_string()),
(DatabaseId::new(0), TenantId::new(2), "events".to_string()),
]
);
}
#[test]
fn uncommitted_partition_directory_is_ignored() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("ts-1_2")).expect("mkdir");
let registry = read_registry(dir.path(), None).expect("read registry");
assert_eq!(registry.partition_count(), 0);
}
#[test]
fn corrupt_partition_meta_is_an_error() {
let dir = tempfile::tempdir().expect("tempdir");
let part = dir.path().join("ts-1_2");
std::fs::create_dir_all(&part).expect("mkdir");
std::fs::write(part.join("partition.meta"), b"{ not json").expect("write meta");
assert!(
read_registry(dir.path(), None).is_err(),
"an undecodable committed meta must surface, not be skipped"
);
}
#[test]
fn load_ts_registries_is_fail_stop_on_a_corrupt_partition_meta() {
use std::sync::Arc;
use nodedb_bridge::buffer::RingBuffer;
use crate::bridge::dispatch::{BridgeRequest, BridgeResponse};
let dir = tempfile::tempdir().expect("tempdir");
let part = dir
.path()
.join("ts")
.join("0")
.join("1")
.join("metrics")
.join("ts-1_2");
std::fs::create_dir_all(&part).expect("mkdir");
std::fs::write(part.join("partition.meta"), b"{ not json").expect("write meta");
let (req_tx, req_rx) = RingBuffer::channel::<BridgeRequest>(64);
let (resp_tx, _resp_rx) = RingBuffer::channel::<BridgeResponse>(64);
drop(req_tx); let mut core = CoreLoop::open(
0,
req_rx,
resp_tx,
dir.path(),
Arc::new(nodedb_types::OrdinalClock::new()),
)
.expect("CoreLoop::open");
assert!(
core.load_ts_registries().is_err(),
"a committed but undecodable partition.meta must fail the boot, not \
be logged and skipped"
);
}
}