use std::fs;
use std::sync::Arc;
use std::time::Duration;
use beamr::module::ModuleRegistry;
use beamr::scheduler::{Scheduler, SchedulerConfig};
use crate::shard::router::{ShardMode, ShardRouter};
use crate::sync::scheduler::{
NoopSyncPullTrigger, SyncSchedulerConfig, SyncSchedulerError, SyncSchedulerHandle,
};
use super::config::{ensure_uninitialised, validate_database_config, write_config};
use super::lock::DataDirLock;
use super::{Database, DatabaseConfig, DatabaseError};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
struct DatabaseParts {
config: DatabaseConfig,
scheduler: Arc<Scheduler>,
router: ShardRouter,
sync_schedulers: Vec<SyncSchedulerHandle>,
}
fn assemble(parts: DatabaseParts, lock: DataDirLock) -> Database {
Database {
config: parts.config,
scheduler: parts.scheduler,
router: parts.router,
sync_schedulers: parts.sync_schedulers,
distribution: None,
owner_stamps: super::owner_stamp::OwnerStamps::default(),
lock,
timeout: DEFAULT_TIMEOUT,
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum StartupMode {
Create,
Open,
}
impl StartupMode {
const fn shard_mode(self) -> ShardMode {
match self {
Self::Create => ShardMode::Create,
Self::Open => ShardMode::Open,
}
}
}
pub(super) const D0_ANCHOR_REMEDY: &str = crate::fence::D0_ANCHOR_REMEDY;
pub(super) fn initialise_database(config: DatabaseConfig) -> Result<Database, DatabaseError> {
let created_dir = !config.data_dir.exists();
create_data_dir(&config.data_dir)?;
sync_new_dir_entry(&config.data_dir)?;
#[cfg(test)]
if take_inject_foreign_config() {
inject_foreign_completed_config(&config.data_dir)?;
}
let lock = DataDirLock::acquire(&config.data_dir)?;
let data_dir = config.data_dir.clone();
if let Err(error) = ensure_uninitialised(&config.data_dir) {
drop(lock);
return Err(error);
}
match install_and_start(config) {
Ok(parts) => Ok(assemble(parts, lock)),
Err(error) => {
if created_dir {
drop(fs::remove_dir_all(&data_dir));
}
drop(lock);
Err(error)
}
}
}
fn install_and_start(config: DatabaseConfig) -> Result<DatabaseParts, DatabaseError> {
write_config(&config)?;
start_database(config, StartupMode::Create)
}
pub(super) fn open_database(config: DatabaseConfig) -> Result<Database, DatabaseError> {
let lock = DataDirLock::acquire(&config.data_dir)?;
sync_new_dir_entry(&config.data_dir)?;
start_database(config, StartupMode::Open).map(|parts| assemble(parts, lock))
}
fn create_data_dir(data_dir: &std::path::Path) -> Result<(), DatabaseError> {
match fs::metadata(data_dir) {
Ok(metadata) if metadata.is_dir() => Ok(()),
Ok(_metadata) => Err(DatabaseError::DirectoryCreate(std::io::Error::other(
format!(
"data directory path is not a directory: {}",
data_dir.display()
),
))),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
#[cfg(test)]
let reservation = crate::fence::journal::reserve_create_dir(data_dir);
match fs::create_dir(data_dir) {
Ok(()) => {
#[cfg(test)]
reservation.commit();
Ok(())
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
#[cfg(test)]
reservation.cancel();
Ok(())
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
#[cfg(test)]
reservation.cancel();
Err(DatabaseError::DirectoryCreate(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"data directory parent does not exist: {} — {D0_ANCHOR_REMEDY}",
data_dir.display()
),
)))
}
Err(error) => {
#[cfg(test)]
reservation.cancel();
Err(DatabaseError::DirectoryCreate(error))
}
}
}
Err(error) => Err(DatabaseError::DirectoryCreate(error)),
}
}
fn start_database(
config: DatabaseConfig,
mode: StartupMode,
) -> Result<DatabaseParts, DatabaseError> {
validate_database_config(&config)?;
let scheduler = create_scheduler()?;
let router = build_router(&scheduler, &config, mode)?;
let sync_schedulers = match spawn_sync_schedulers(&scheduler, &config, &router) {
Ok(handles) => handles,
Err(error) => {
router.shutdown_all(DEFAULT_TIMEOUT);
return Err(error);
}
};
Ok(DatabaseParts {
config,
scheduler,
router,
sync_schedulers,
})
}
fn sync_new_dir_entry(data_dir: &std::path::Path) -> Result<(), DatabaseError> {
let Some(parent) = data_dir.parent() else {
return Ok(());
};
crate::fence::sync_dir_entry(parent).map_err(DatabaseError::DirectoryCreate)
}
#[cfg(test)]
thread_local! {
static INJECT_FOREIGN_CONFIG: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(super) fn inject_foreign_config_next_create() {
INJECT_FOREIGN_CONFIG.with(|flag| flag.set(true));
}
#[cfg(test)]
fn take_inject_foreign_config() -> bool {
INJECT_FOREIGN_CONFIG.with(std::cell::Cell::take)
}
#[cfg(test)]
pub(super) const FOREIGN_SHARD_COUNT: usize = 9;
#[cfg(test)]
fn inject_foreign_completed_config(data_dir: &std::path::Path) -> Result<(), DatabaseError> {
write_config(&DatabaseConfig {
data_dir: data_dir.to_path_buf(),
shard_count: FOREIGN_SHARD_COUNT,
distributed: None,
})
}
fn create_scheduler() -> Result<Arc<Scheduler>, DatabaseError> {
Scheduler::new(SchedulerConfig::default(), Arc::new(ModuleRegistry::new()))
.map(Arc::new)
.map_err(DatabaseError::ShardSpawn)
}
fn build_router(
scheduler: &Arc<Scheduler>,
config: &DatabaseConfig,
mode: StartupMode,
) -> Result<ShardRouter, DatabaseError> {
ShardRouter::new(
Arc::clone(scheduler),
&config.data_dir,
config.shard_count,
mode.shard_mode(),
)
.ok_or(DatabaseError::InvalidShardCount)
}
fn spawn_sync_schedulers(
scheduler: &Arc<Scheduler>,
config: &DatabaseConfig,
router: &ShardRouter,
) -> Result<Vec<SyncSchedulerHandle>, DatabaseError> {
let Some(distributed) = &config.distributed else {
return Ok(Vec::new());
};
let topology = distributed
.topology
.clone()
.ok_or(DatabaseError::MissingSyncTopology)?;
let scheduler_config = SyncSchedulerConfig::new(
distributed.local_node.clone(),
distributed.nodes.clone(),
topology,
config.shard_count,
Duration::from_millis(distributed.sync_interval),
);
SyncSchedulerHandle::spawn_with_shard_source(
Arc::clone(scheduler),
scheduler_config,
Arc::new(NoopSyncPullTrigger),
Arc::new(router.membership()),
DEFAULT_TIMEOUT,
)
.map(|handle| vec![handle])
.map_err(map_sync_scheduler_error)
}
fn map_sync_scheduler_error(error: SyncSchedulerError) -> DatabaseError {
match error {
SyncSchedulerError::Spawn(message) => DatabaseError::SyncSchedulerSpawn(message),
other => DatabaseError::SyncSchedulerError(other.to_string()),
}
}