use std::collections::BTreeMap;
use std::fmt;
use std::sync::{Arc, Mutex, MutexGuard};
use crate::store::NodeStore;
use crate::tree::{Cursor, Hash, TreeError};
use crate::wal::{LookupResult, WalBuffer};
use super::operation_error::BranchGetError;
use super::registry::BranchRegistryGuard;
use super::time::Timestamp;
pub use crate::ids::ShardId;
pub const DEFAULT_SHARD_ID: ShardId = 0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BranchError {
NoShards,
DuplicateShard { shard_id: ShardId },
UnknownShard { shard_id: ShardId },
BufferPoisoned { shard_id: ShardId },
UnregisteredParent,
Tree(TreeError),
}
impl fmt::Display for BranchError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoShards => write!(formatter, "branch requires at least one shard"),
Self::DuplicateShard { shard_id } => write!(
formatter,
"branch shard {shard_id} was provided more than once"
),
Self::UnknownShard { shard_id } => {
write!(formatter, "branch shard {shard_id} is not present")
}
Self::BufferPoisoned { shard_id } => {
write!(
formatter,
"branch shard {shard_id} shared state is poisoned"
)
}
Self::UnregisteredParent => write!(
formatter,
"fork_from requires a registered parent handle (its heads carry no prune pin \
otherwise); fork the parent via a registered constructor or use fork_at"
),
Self::Tree(error) => write!(formatter, "branch tree read error: {error}"),
}
}
}
impl std::error::Error for BranchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Tree(error) => Some(error),
Self::NoShards
| Self::DuplicateShard { .. }
| Self::UnknownShard { .. }
| Self::BufferPoisoned { .. }
| Self::UnregisteredParent => None,
}
}
}
impl From<TreeError> for BranchError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefBinding {
pub name: String,
pub created: Timestamp,
pub seq: u64,
}
#[derive(Debug, Clone)]
pub struct BranchHandle {
primary_shard: ShardId,
primary: BranchShard,
additional_shards: BTreeMap<ShardId, BranchShard>,
registry_guard: Option<Arc<BranchRegistryGuard>>,
pub(crate) binding: Option<Arc<Mutex<RefBinding>>>,
}
impl BranchHandle {
#[must_use]
pub fn new(root: Hash) -> Self {
Self {
primary_shard: DEFAULT_SHARD_ID,
primary: BranchShard::new(root),
additional_shards: BTreeMap::new(),
registry_guard: None,
binding: None,
}
}
pub fn from_shard_roots<I>(roots: I) -> Result<Self, BranchError>
where
I: IntoIterator<Item = (ShardId, Hash)>,
{
Self::from_anchor_head_pairs(
roots
.into_iter()
.map(|(shard_id, root)| (shard_id, root, root)),
)
}
pub(crate) fn from_anchor_head_pairs<I>(entries: I) -> Result<Self, BranchError>
where
I: IntoIterator<Item = (ShardId, Hash, Hash)>,
{
let mut iter = entries.into_iter();
let Some((primary_shard, primary_anchor, primary_head)) = iter.next() else {
return Err(BranchError::NoShards);
};
let mut additional_shards = BTreeMap::new();
for (shard_id, anchor, head) in iter {
if shard_id == primary_shard
|| additional_shards
.insert(shard_id, BranchShard::with_head(anchor, head))
.is_some()
{
return Err(BranchError::DuplicateShard { shard_id });
}
}
Ok(Self {
primary_shard,
primary: BranchShard::with_head(primary_anchor, primary_head),
additional_shards,
registry_guard: None,
binding: None,
})
}
#[must_use]
pub const fn fork_point(&self) -> Hash {
self.primary.fork_point
}
#[must_use]
pub fn current_root(&self) -> Hash {
self.primary.current_root()
}
#[must_use]
pub const fn primary_shard(&self) -> ShardId {
self.primary_shard
}
#[must_use]
pub const fn is_named(&self) -> bool {
self.binding.is_some()
}
#[must_use]
pub fn shard_count(&self) -> usize {
self.additional_shards.len().saturating_add(1)
}
#[must_use]
pub fn contains_shard(&self, shard_id: ShardId) -> bool {
self.shard(shard_id).is_some()
}
#[must_use]
pub fn shard_fork_point(&self, shard_id: ShardId) -> Option<Hash> {
self.shard(shard_id).map(|shard| shard.fork_point)
}
#[must_use]
pub fn shard_current_root(&self, shard_id: ShardId) -> Option<Hash> {
self.shard(shard_id).map(BranchShard::current_root)
}
pub fn shard_is_dirty(&self, shard_id: ShardId) -> Result<bool, BranchError> {
let state = self.lock_state(shard_id)?;
Ok(!state.buffer.is_empty())
}
pub fn shard_ids(&self) -> impl Iterator<Item = ShardId> + '_ {
std::iter::once(self.primary_shard).chain(self.additional_shards.keys().copied())
}
pub fn put<K, V>(&self, shard_id: ShardId, key: K, value: V) -> Result<(), BranchError>
where
K: AsRef<[u8]>,
V: AsRef<[u8]>,
{
let mut state = self.lock_state(shard_id)?;
state.buffer.put(key, value);
drop(state);
Ok(())
}
pub fn delete<K>(&self, shard_id: ShardId, key: K) -> Result<(), BranchError>
where
K: AsRef<[u8]>,
{
let mut state = self.lock_state(shard_id)?;
state.buffer.delete(key);
drop(state);
Ok(())
}
pub fn get<S>(
&self,
shard_id: ShardId,
store: &S,
key: &[u8],
) -> Result<Option<Vec<u8>>, BranchError>
where
S: NodeStore + ?Sized,
{
self.get_with_source(shard_id, store, key)
.map_err(|error| match error {
BranchGetError::Branch(error) => error,
BranchGetError::Tree(error) => BranchError::Tree(error.into_tree_error()),
})
}
pub(crate) fn get_with_source<S>(
&self,
shard_id: ShardId,
store: &S,
key: &[u8],
) -> Result<Option<Vec<u8>>, BranchGetError<S::Error>>
where
S: NodeStore + ?Sized,
{
let (lookup, root) = {
let state = self.lock_state(shard_id).map_err(BranchGetError::Branch)?;
(state.buffer.get(key), state.current_root)
};
match lookup {
LookupResult::BufferedValue(value) => Ok(Some(value)),
LookupResult::BufferedDelete => Ok(None),
LookupResult::NotBuffered => Cursor::new(store, root)
.get_with_source(key)
.map_err(BranchGetError::Tree),
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
pub(crate) fn fork_points(&self) -> impl Iterator<Item = Hash> + '_ {
std::iter::once(self.primary.fork_point).chain(
self.additional_shards
.values()
.map(|shard| shard.fork_point),
)
}
pub(crate) fn with_registry_guard(mut self, guard: BranchRegistryGuard) -> Self {
self.registry_guard = Some(Arc::new(guard));
self
}
pub(crate) fn registry_guard(&self) -> Option<&BranchRegistryGuard> {
self.registry_guard.as_deref()
}
pub(crate) fn with_binding(mut self, binding: RefBinding) -> Self {
self.binding = Some(Arc::new(Mutex::new(binding)));
self
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
pub(crate) fn binding_snapshot(&self) -> Option<RefBinding> {
self.binding.as_deref().map(|binding| match binding.lock() {
Ok(bound) => bound.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
})
}
pub(crate) fn lock_states_ascending(
&self,
) -> Result<Vec<(ShardId, MutexGuard<'_, ShardState>)>, BranchError> {
let mut shards: Vec<(ShardId, &BranchShard)> =
std::iter::once((self.primary_shard, &self.primary))
.chain(
self.additional_shards
.iter()
.map(|(shard_id, shard)| (*shard_id, shard)),
)
.collect();
shards.sort_unstable_by_key(|(shard_id, _shard)| *shard_id);
shards
.into_iter()
.map(|(shard_id, shard)| match shard.state.lock() {
Ok(guard) => Ok((shard_id, guard)),
Err(poisoned) => {
drop(poisoned.into_inner());
Err(BranchError::BufferPoisoned { shard_id })
}
})
.collect()
}
fn shard(&self, shard_id: ShardId) -> Option<&BranchShard> {
if shard_id == self.primary_shard {
Some(&self.primary)
} else {
self.additional_shards.get(&shard_id)
}
}
fn lock_state(&self, shard_id: ShardId) -> Result<MutexGuard<'_, ShardState>, BranchError> {
let shard = self
.shard(shard_id)
.ok_or(BranchError::UnknownShard { shard_id })?;
match shard.state.lock() {
Ok(guard) => Ok(guard),
Err(poisoned) => {
drop(poisoned.into_inner());
Err(BranchError::BufferPoisoned { shard_id })
}
}
}
}
#[derive(Debug, Clone)]
struct BranchShard {
fork_point: Hash,
state: Arc<Mutex<ShardState>>,
}
#[derive(Debug)]
pub(crate) struct ShardState {
pub(crate) current_root: Hash,
pub(crate) buffer: WalBuffer,
}
impl BranchShard {
fn new(root: Hash) -> Self {
Self::with_head(root, root)
}
fn with_head(fork_point: Hash, head: Hash) -> Self {
Self {
fork_point,
state: Arc::new(Mutex::new(ShardState {
current_root: head,
buffer: WalBuffer::new(),
})),
}
}
fn current_root(&self) -> Hash {
match self.state.lock() {
Ok(guard) => guard.current_root,
Err(poisoned) => poisoned.into_inner().current_root,
}
}
}
#[cfg(test)]
#[path = "handle_tests.rs"]
mod tests;