use std::fs;
use std::path::Path;
use std::sync::Arc;
use kmp_domain::PortError;
use super::engine::redb::RedbEngine;
use super::engine::{Engine, ReadTx, Table, WriteTx};
use super::format_version::{self, StorageEngine};
#[derive(Debug, Clone)]
pub struct EmbeddedKernelStore {
engine: Arc<dyn Engine>,
}
impl EmbeddedKernelStore {
pub fn open(data_dir: &Path) -> Result<Self, PortError> {
Self::open_as(data_dir, None)
}
pub fn open_with_engine(data_dir: &Path, engine: StorageEngine) -> Result<Self, PortError> {
Self::open_as(data_dir, Some(engine))
}
pub fn engine_of(data_dir: &Path) -> Result<StorageEngine, PortError> {
format_version::check_or_stamp_as(data_dir, None)
}
fn open_as(data_dir: &Path, wanted: Option<StorageEngine>) -> Result<Self, PortError> {
fs::create_dir_all(data_dir).map_err(|error| {
PortError::Unavailable(format!(
"embedded store could not create data dir `{}`: {error}",
data_dir.display()
))
})?;
let engine = format_version::check_or_stamp_as(data_dir, wanted)?;
let store_file = format_version::store_file_path_for(data_dir, engine);
fs::create_dir_all(store_file.parent().expect("store file has a parent")).map_err(
|error| {
PortError::Unavailable(format!(
"embedded store could not create store dir under `{}`: {error}",
data_dir.display()
))
},
)?;
Self::open_store_file(&store_file, engine)
}
pub(crate) fn open_store_file(
store_file: &Path,
engine: StorageEngine,
) -> Result<Self, PortError> {
let engine: Arc<dyn Engine> = match engine {
StorageEngine::Redb => Arc::new(RedbEngine::open_file(store_file)?),
#[cfg(feature = "sqlite")]
StorageEngine::Sqlite => {
Arc::new(super::engine::sqlite::SqliteEngine::open_file(store_file)?)
}
#[cfg(not(feature = "sqlite"))]
StorageEngine::Sqlite => {
return Err(PortError::Unavailable(format!(
"embedded store `{}` needs the sqlite engine, which this binary was built \
without",
store_file.display()
)));
}
};
Ok(Self { engine })
}
pub(crate) fn begin_write(&self) -> Result<Box<dyn WriteTx + '_>, PortError> {
self.engine.begin_write()
}
pub(crate) fn begin_read(&self) -> Result<Box<dyn ReadTx + '_>, PortError> {
self.engine.begin_read()
}
pub(crate) async fn run<T, F>(&self, task: F) -> Result<T, PortError>
where
T: Send + 'static,
F: FnOnce(&EmbeddedKernelStore) -> Result<T, PortError> + Send + 'static,
{
let store = self.clone();
tokio::task::spawn_blocking(move || task(&store))
.await
.map_err(|error| {
PortError::Unavailable(format!("embedded store worker failed: {error}"))
})?
}
pub async fn event_log_stats(&self) -> Result<(u64, u64), PortError> {
self.run(|store| {
let tx = store.begin_read()?;
let count = tx.count(Table::EventLog)?;
let last_sequence = tx.last_u64(Table::EventLog)?.map_or(0, |(key, _)| key);
Ok((count, last_sequence))
})
.await
}
pub fn compact_data_dir(data_dir: &Path) -> Result<bool, PortError> {
let engine = format_version::check_or_stamp(data_dir)?;
let store_file = format_version::store_file_path_for(data_dir, engine);
match engine {
StorageEngine::Redb => RedbEngine::compact_file(&store_file),
#[cfg(feature = "sqlite")]
StorageEngine::Sqlite => super::engine::sqlite::SqliteEngine::compact_file(&store_file),
#[cfg(not(feature = "sqlite"))]
StorageEngine::Sqlite => unreachable!("the format gate refuses uncompiled engines"),
}
}
}
pub(crate) fn aggregate_key(root_node_id: &str, role: &str) -> String {
format!("{root_node_id}\u{1f}{role}")
}