use super::*;
#[derive(Debug, Clone)]
pub struct EventsSplitConfig {
pub db_path: PathBuf,
pub socket_path: Option<PathBuf>,
}
#[cfg(unix)]
pub(super) type ClientMap = std::collections::HashMap<PathBuf, Arc<EventsSplitClient>>;
pub(super) type BackendMap = std::collections::HashMap<PathBuf, (bool, Arc<StorageBackend>)>;
#[cfg(unix)]
pub(super) fn client_registry() -> &'static std::sync::Mutex<ClientMap> {
static REGISTRY: std::sync::OnceLock<std::sync::Mutex<ClientMap>> = std::sync::OnceLock::new();
REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub(super) fn direct_backend_registry() -> &'static std::sync::Mutex<BackendMap> {
static REGISTRY: std::sync::OnceLock<std::sync::Mutex<BackendMap>> = std::sync::OnceLock::new();
REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
#[cfg(any(test, feature = "test-internals"))]
#[doc(hidden)]
pub struct TestRegistryGuard<'a> {
pub(super) root: &'a Path,
pub(super) canonical_root: PathBuf,
}
#[cfg(any(test, feature = "test-internals"))]
impl<'a> TestRegistryGuard<'a> {
pub fn new(root: &'a Path) -> Self {
Self {
root,
canonical_root: std::fs::canonicalize(root).expect("existing unique fixture root"),
}
}
pub(super) fn remove_entries<T>(
&self,
registry: &std::sync::Mutex<std::collections::HashMap<PathBuf, T>>,
) -> Vec<T> {
let mut entries = registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let keys: Vec<_> = entries
.keys()
.filter(|path| path.starts_with(self.root) || path.starts_with(&self.canonical_root))
.cloned()
.collect();
keys.into_iter()
.filter_map(|key| entries.remove(&key))
.collect()
}
}
#[cfg(any(test, feature = "test-internals"))]
impl Drop for TestRegistryGuard<'_> {
fn drop(&mut self) {
let backends = self.remove_entries(direct_backend_registry());
#[cfg(unix)]
let clients = self.remove_entries(client_registry());
drop(backends);
#[cfg(unix)]
drop(clients);
}
}
#[cfg(unix)]
pub fn client_for(socket_path: &Path) -> crate::error::RuntimeResult<Arc<EventsSplitClient>> {
let mut registry = client_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(existing) = registry.get(socket_path) {
return Ok(Arc::clone(existing));
}
let client = EventsSplitClient::new(socket_path.to_path_buf())?;
registry.insert(socket_path.to_path_buf(), Arc::clone(&client));
Ok(client)
}
pub fn direct_backend_for(db_path: &Path) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
direct_backend_with_max_readers(db_path, false, None)
}
pub fn direct_backend_read_only_for(
db_path: &Path,
) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
direct_backend_with_max_readers(db_path, true, None)
}
pub(crate) fn direct_backend_with_max_readers(
db_path: &Path,
read_only: bool,
max_readers: Option<usize>,
) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
let mut config = crate::RuntimeConfig {
db_path: Some(db_path.to_path_buf()),
..crate::RuntimeConfig::no_embeddings()
};
let wal_ceiling = config.resolve_wal_ceiling_policy(read_only)?;
let disk_guard = config.resolve_disk_guard_policy(read_only)?;
direct_backend_with_policies(
db_path,
read_only,
max_readers,
wal_ceiling,
disk_guard,
config.volume_lock_dir,
)
}
pub(crate) fn direct_backend_with_policies(
db_path: &Path,
read_only: bool,
max_readers: Option<usize>,
wal_ceiling: WalCeilingPolicy,
disk_guard: Option<khive_db::EffectiveDiskGuardConfig>,
volume_lock_dir: Option<PathBuf>,
) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
wal_ceiling.validate_static(true, true, read_only)?;
if !read_only {
disk_guard
.ok_or_else(|| {
crate::error::RuntimeError::Internal("missing events disk policy".into())
})?
.validate()?;
khive_db::require_volume_lock_dir(volume_lock_dir.clone())?;
}
let mut registry = direct_backend_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let absolute_path = absolutize(db_path);
let db_path = absolute_path.as_path();
refuse_events_db_symlinks(db_path)
.map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
#[cfg(unix)]
if !read_only {
if let Some(parent) = db_path.parent().filter(|p| !p.as_os_str().is_empty()) {
let _ = std::fs::create_dir_all(parent);
}
}
#[cfg(unix)]
ensure_events_db_parent_trusted(db_path)
.map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
let key = std::fs::canonicalize(db_path)
.or_else(|error| {
if error.kind() != std::io::ErrorKind::NotFound {
return Err(error);
}
let parent = db_path.parent().ok_or(error)?;
let name = db_path.file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"events path has no file name",
)
})?;
std::fs::canonicalize(parent).map(|parent| parent.join(name))
})
.map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
if let Some((existing_read_only, existing)) = registry.get(&key) {
if *existing_read_only != read_only {
let existing_mode = if *existing_read_only {
"read-only"
} else {
"writable"
};
let requested_mode = if read_only { "read-only" } else { "writable" };
return Err(crate::error::RuntimeError::InvalidInput(format!(
"events database {} is already open {existing_mode} in this process; cannot \
open it {requested_mode}; read-only events access requires a separate frozen snapshot",
key.display()
)));
}
let numbers = |p: Option<khive_db::EffectiveDiskGuardConfig>| {
p.map(|p| (p.reserve_bytes, p.guard_deadline_ms))
};
if numbers(existing.pool().effective_disk_guard_config()) != numbers(disk_guard) {
return Err(crate::error::RuntimeError::Internal(
"events database is already open with a different disk reserve/deadline policy"
.into(),
));
}
if !read_only && existing.pool().config().volume_lock_dir != volume_lock_dir {
return Err(crate::error::RuntimeError::Internal(
"events database is already open with a different volume-lock directory".into(),
));
}
let existing_bytes = existing.pool().config().wal_ceiling.bytes;
if existing_bytes != wal_ceiling.bytes {
return Err(khive_db::SqliteError::InvalidConfig(format!(
"events database {} is already open with wal_ceiling_bytes={existing_bytes}; \
requested {}; drain and restart before changing the WAL ceiling",
key.display(),
wal_ceiling.bytes
))
.into());
}
return Ok(Arc::clone(existing));
}
let db_path = key.as_path();
#[cfg(unix)]
if !read_only {
use std::os::unix::fs::OpenOptionsExt;
let _ = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(db_path);
let _before_open = harden_events_db_sidecars(db_path)
.map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
}
let backend = Arc::new(if read_only {
StorageBackend::sqlite_read_only_with_max_readers_and_wal_ceiling(
db_path,
max_readers,
wal_ceiling,
)?
} else {
StorageBackend::sqlite_with_max_readers_and_policies(
db_path,
max_readers,
wal_ceiling,
disk_guard.ok_or_else(|| {
crate::error::RuntimeError::Internal("missing events disk policy".into())
})?,
khive_db::require_volume_lock_dir(volume_lock_dir)?,
)?
});
registry.insert(key, (read_only, Arc::clone(&backend)));
Ok(backend)
}
#[cfg(unix)]
pub fn forwarding_metrics(socket_path: &Path) -> Option<EventsForwardingMetrics> {
let registry = client_registry()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
registry.get(socket_path).map(|client| client.metrics())
}