use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use beamr::scheduler::Scheduler;
use crate::shard::actor::ShardHandle;
use crate::ttl::sweep::SweepHandle;
const SHARD_STORE_DIR: &str = "store";
const SHARD_WAL_FILE: &str = "shard.wal";
#[derive(Clone, Copy, Debug)]
pub enum ShardMode {
Create,
Open,
}
#[derive(Clone, Debug)]
pub struct SweepConfig {
pub interval: Duration,
pub command_timeout: Duration,
}
#[derive(Debug)]
struct MaterialisedShard {
handle: ShardHandle,
sweep: Option<SweepHandle>,
}
struct SpawnContext {
scheduler: Arc<Scheduler>,
data_dir: PathBuf,
mode: ShardMode,
sweep: Option<SweepConfig>,
}
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)
.field("sweep", &self.sweep)
.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,
mode: ShardMode,
sweep: Option<SweepConfig>,
) -> 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(),
mode,
sweep,
},
})
}
pub(crate) fn membership(&self) -> MaterialisedMembership {
MaterialisedMembership {
materialised: Arc::clone(&self.materialised),
}
}
pub(crate) fn shard_for(&self, key: &[u8]) -> 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 % self.shard_count as u64) as usize
}
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>) {
self.materialised
.lock()
.map(|map| {
let ids = map.keys().copied().collect();
let handles = map.values().map(|shard| shard.handle.clone()).collect();
(ids, handles)
})
.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 Some(sweep) = shard.sweep
&& let Err(error) = sweep.shutdown(timeout)
{
log::debug!(
"router sweep shutdown skipped for supervisor pid {}: {error}",
sweep.supervisor_pid()
);
}
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);
std::fs::create_dir_all(&shard_dir).map_err(|error| MaterialiseError {
shard_id,
message: format!("shard directory create failed: {error}"),
})?;
let store_dir = shard_dir.join(SHARD_STORE_DIR);
let wal_path = shard_dir.join(SHARD_WAL_FILE);
let handle = ShardHandle::spawn(Arc::clone(&self.scheduler), &store_dir, &wal_path)
.map_err(|error| MaterialiseError {
shard_id,
message: format!("shard spawn failed: {error:?}"),
})?;
let sweep = match &self.sweep {
None => None,
Some(config) => Some(
SweepHandle::spawn(
Arc::clone(&self.scheduler),
store_dir,
wal_path,
handle.clone(),
config.interval,
config.command_timeout,
)
.map_err(|error| {
drop(handle.shutdown(config.command_timeout));
MaterialiseError {
shard_id,
message: format!("sweep spawn failed: {error}"),
}
})?,
),
};
Ok(MaterialisedShard { handle, sweep })
}
}
fn poisoned(shard_id: usize) -> MaterialiseError {
MaterialiseError {
shard_id,
message: "shard router map lock poisoned".to_owned(),
}
}
fn shard_dir(data_dir: &Path, index: usize) -> PathBuf {
data_dir.join(format!("shard-{index}"))
}
#[cfg(test)]
#[path = "router_tests.rs"]
mod tests;