use beamr::scheduler::Scheduler;
use crate::db::root_advance::RootAdvanceSeam;
use crate::shard::actor::ShardHandle;
use crate::shard::commit_state::ShardCommitState;
use crate::tree::TreePolicy;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
pub const SHARD_STORE_DIR: &str = "store";
pub const SHARD_WAL_FILE: &str = "shard.wal";
#[derive(Clone, Copy, Debug)]
pub enum ShardMode {
Create,
Open,
}
#[derive(Debug)]
struct MaterialisedShard {
handle: ShardHandle,
commit_state: Arc<ShardCommitState>,
}
struct SpawnContext {
scheduler: Arc<Scheduler>,
data_dir: PathBuf,
policy: TreePolicy,
mode: ShardMode,
seam: Arc<RootAdvanceSeam>,
}
impl std::fmt::Debug for SpawnContext {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("SpawnContext")
.field("data_dir", &self.data_dir)
.field("mode", &self.mode)
.finish_non_exhaustive()
}
}
type MaterialisedMap = Arc<Mutex<BTreeMap<usize, MaterialisedShard>>>;
#[derive(Debug)]
pub struct ShardRouter {
shard_count: usize,
materialised: MaterialisedMap,
context: SpawnContext,
}
#[derive(Clone, Debug)]
pub struct MaterialisedMembership {
materialised: MaterialisedMap,
}
impl MaterialisedMembership {
pub(crate) fn shard_ids(&self) -> Vec<usize> {
self.materialised
.lock()
.map(|map| map.keys().copied().collect())
.unwrap_or_default()
}
}
impl crate::sync::scheduler::SyncShardSource for MaterialisedMembership {
fn shards_to_sync(&self) -> Vec<usize> {
self.shard_ids()
}
}
#[derive(Debug)]
pub struct MaterialiseError {
pub shard_id: usize,
pub message: String,
}
impl ShardRouter {
pub(crate) fn new(
scheduler: Arc<Scheduler>,
data_dir: &Path,
shard_count: usize,
policy: TreePolicy,
mode: ShardMode,
seam: Arc<RootAdvanceSeam>,
) -> Option<Self> {
if shard_count == 0 {
return None;
}
Some(Self {
shard_count,
materialised: Arc::new(Mutex::new(BTreeMap::new())),
context: SpawnContext {
scheduler,
data_dir: data_dir.to_path_buf(),
policy,
mode,
seam,
},
})
}
pub(crate) fn membership(&self) -> MaterialisedMembership {
MaterialisedMembership {
materialised: Arc::clone(&self.materialised),
}
}
pub(crate) fn shard_for(&self, key: &[u8]) -> usize {
shard_index_for(key, self.shard_count)
}
pub(crate) fn handle_for(&self, key: &[u8]) -> Result<ShardHandle, MaterialiseError> {
self.handle_for_shard(self.shard_for(key))
}
pub(crate) fn handle_for_shard(
&self,
shard_id: usize,
) -> Result<ShardHandle, MaterialiseError> {
if shard_id >= self.shard_count {
return Err(MaterialiseError {
shard_id,
message: format!(
"shard id {shard_id} out of range for shard_count {}",
self.shard_count
),
});
}
self.materialise(shard_id)
}
fn materialise(&self, shard_id: usize) -> Result<ShardHandle, MaterialiseError> {
let mut map = self.materialised.lock().map_err(|_| poisoned(shard_id))?;
let handle = if let Some(existing) = map.get(&shard_id) {
existing.handle.clone()
} else {
let shard = self.context.spawn_shard(shard_id)?;
let handle = shard.handle.clone();
map.insert(shard_id, shard);
handle
};
drop(map);
Ok(handle)
}
pub(crate) fn materialised_handles(&self) -> Vec<ShardHandle> {
self.materialised
.lock()
.map(|map| map.values().map(|shard| shard.handle.clone()).collect())
.unwrap_or_default()
}
#[cfg(test)]
pub(crate) fn materialised_shard_ids(&self) -> Vec<usize> {
self.materialised
.lock()
.map(|map| map.keys().copied().collect())
.unwrap_or_default()
}
pub(crate) fn materialised_snapshot(
&self,
) -> (Vec<usize>, Vec<ShardHandle>, Vec<Arc<ShardCommitState>>) {
self.materialised
.lock()
.map(|map| {
let ids = map.keys().copied().collect();
let handles = map.values().map(|shard| shard.handle.clone()).collect();
let cells = map
.values()
.map(|shard| Arc::clone(&shard.commit_state))
.collect();
(ids, handles, cells)
})
.unwrap_or_default()
}
pub(crate) fn shutdown_all(&self, timeout: Duration) {
let drained: Vec<MaterialisedShard> = match self.materialised.lock() {
Ok(mut map) => std::mem::take(&mut *map).into_values().collect(),
Err(_) => return,
};
for shard in drained {
if let Err(error) = shard.handle.shutdown(timeout) {
log::debug!(
"router shard shutdown skipped for pid {}: {error}",
shard.handle.pid()
);
}
}
}
}
impl SpawnContext {
fn spawn_shard(&self, shard_id: usize) -> Result<MaterialisedShard, MaterialiseError> {
let shard_dir = shard_dir(&self.data_dir, shard_id);
Self::ensure_shard_dir(&shard_dir, shard_id)?;
crate::fence::sync_dir_entry(&self.data_dir).map_err(|error| MaterialiseError {
shard_id,
message: format!("data directory fence failed: {error}"),
})?;
let store_dir = shard_dir.join(SHARD_STORE_DIR);
let wal_path = shard_dir.join(SHARD_WAL_FILE);
let commit_state = self.seam.commit_state(shard_id);
let handle = ShardHandle::spawn(
Arc::clone(&self.scheduler),
&store_dir,
&wal_path,
shard_id,
self.policy,
Arc::clone(&self.seam),
)
.map_err(|error| MaterialiseError {
shard_id,
message: format!("shard spawn failed: {error:?}"),
})?;
Ok(MaterialisedShard {
handle,
commit_state,
})
}
fn ensure_shard_dir(shard_dir: &Path, shard_id: usize) -> Result<(), MaterialiseError> {
let map_io = |error: std::io::Error| MaterialiseError {
shard_id,
message: format!("shard directory create failed: {error}"),
};
match std::fs::metadata(shard_dir) {
Ok(metadata) if metadata.is_dir() => Ok(()),
Ok(_metadata) => Err(MaterialiseError {
shard_id,
message: format!("shard path is not a directory: {}", shard_dir.display()),
}),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
#[cfg(test)]
let reservation = crate::fence::journal::reserve_create_dir(shard_dir);
match std::fs::create_dir(shard_dir) {
Ok(()) => {
#[cfg(test)]
reservation.commit();
Ok(())
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
#[cfg(test)]
reservation.cancel();
Ok(())
}
Err(error) => {
#[cfg(test)]
reservation.cancel();
Err(map_io(error))
}
}
}
Err(error) => Err(map_io(error)),
}
}
}
fn poisoned(shard_id: usize) -> MaterialiseError {
MaterialiseError {
shard_id,
message: "shard router map lock poisoned".to_owned(),
}
}
pub fn shard_index_for(key: &[u8], shard_count: usize) -> usize {
let digest = blake3::hash(key);
let mut prefix = [0_u8; 8];
for (target, source) in prefix.iter_mut().zip(digest.as_bytes().iter()) {
*target = *source;
}
let value = u64::from_be_bytes(prefix);
(value % shard_count as u64) as usize
}
pub fn shard_dir(data_dir: &Path, index: usize) -> PathBuf {
data_dir.join(format!("shard-{index}"))
}
#[cfg(test)]
#[path = "router_tests.rs"]
mod tests;