use std::fmt;
use std::sync::{Mutex, MutexGuard};
use crate::store::NodeStore;
use crate::tree::{Hash, TreeError, batch_mutate_owned};
use crate::wal::{Mutation, WalBuffer};
use super::handle::{BranchError, BranchHandle, RefBinding, ShardId, ShardState};
use super::refstore::{BranchRefError, BranchRefStore};
use super::registry::{BranchRegistry, advance_in};
use super::snapshot::Timestamp;
#[derive(Debug)]
pub enum BranchCommitError {
Unregistered,
UnnamedBranch,
VolatileExtraParents,
UnprotectedAnchor {
root: Hash,
},
Branch(BranchError),
Tree(TreeError),
Barrier(String),
Ref(BranchRefError),
}
impl fmt::Display for BranchCommitError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unregistered => write!(
formatter,
"branch handle is not registered against a branch registry; \
fork it via a registering constructor before committing"
),
Self::UnnamedBranch => write!(
formatter,
"durable commit requires a named branch handle from create_branch/open_branch"
),
Self::VolatileExtraParents => write!(
formatter,
"extra parents require a durable commit: volatile commits install no record to carry them"
),
Self::UnprotectedAnchor { root } => write!(
formatter,
"fork anchor {root} is protected by no live branch, no named branch record, and \
no named snapshot, so a prune could reclaim it at any moment; name a snapshot \
at this root first (SnapshotRegistry::name), then fork"
),
Self::Branch(error) => write!(formatter, "branch commit handle error: {error}"),
Self::Tree(error) => write!(formatter, "branch commit tree error: {error}"),
Self::Barrier(reason) => {
write!(
formatter,
"branch commit durability barrier failed: {reason}"
)
}
Self::Ref(error) => write!(formatter, "branch commit ref store error: {error}"),
}
}
}
impl std::error::Error for BranchCommitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Branch(error) => Some(error),
Self::Tree(error) => Some(error),
Self::Ref(error) => Some(error),
Self::Unregistered
| Self::UnnamedBranch
| Self::VolatileExtraParents
| Self::UnprotectedAnchor { .. }
| Self::Barrier(_) => None,
}
}
}
impl From<BranchError> for BranchCommitError {
fn from(error: BranchError) -> Self {
Self::Branch(error)
}
}
impl From<TreeError> for BranchCommitError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
impl From<BranchRefError> for BranchCommitError {
fn from(error: BranchRefError) -> Self {
Self::Ref(error)
}
}
#[derive(Debug)]
pub enum CommitDurability<'a> {
Volatile,
Durable {
refs: &'a mut BranchRefStore,
},
}
#[derive(Debug)]
pub struct CommitRequest<'a> {
pub durability: CommitDurability<'a>,
pub extra_parents: &'a [Hash],
pub timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitOutcome {
pub heads: Vec<(ShardId, Hash)>,
pub seq: Option<u64>,
pub advanced: bool,
}
pub fn commit_branch<S: NodeStore + ?Sized>(
branch: &BranchHandle,
store: &mut S,
registry: &BranchRegistry,
request: CommitRequest<'_>,
) -> Result<CommitOutcome, BranchCommitError> {
let CommitRequest {
durability,
extra_parents,
timestamp,
} = request;
let Some(registry_guard) = branch.registry_guard() else {
return Err(BranchCommitError::Unregistered);
};
if matches!(durability, CommitDurability::Volatile) && !extra_parents.is_empty() {
return Err(BranchCommitError::VolatileExtraParents);
}
let mut states = branch.lock_states_ascending()?;
let mut binding = branch.binding.as_deref().map(lock_binding);
let durable = match durability {
CommitDurability::Volatile => None,
CommitDurability::Durable { refs } => match binding.as_deref() {
Some(bound) => Some((refs, bound.name.clone(), bound.created, bound.seq)),
None => return Err(BranchCommitError::UnnamedBranch),
},
};
let mut counts = registry.lock_counts();
if extra_parents.is_empty() && states.iter().all(|(_, state)| state.buffer.is_empty()) {
if let Some((refs, name, created, seq)) = &durable {
refs.cas_check(name, *created, *seq)?;
}
return Ok(CommitOutcome {
heads: current_heads(&states),
seq: durable.map(|(_, _, _, seq)| seq),
advanced: false,
});
}
let mut materialised: Vec<Option<Hash>> = Vec::with_capacity(states.len());
for (_, state) in &states {
if state.buffer.is_empty() {
materialised.push(None);
continue;
}
let new_root =
batch_mutate_owned(store, state.current_root, buffered_batch(&state.buffer))?;
materialised.push(Some(new_root));
}
let advanced: Vec<(ShardId, Hash, Hash)> = states
.iter()
.zip(&materialised)
.filter_map(|((shard_id, state), new_root)| {
new_root.and_then(|new_root| {
(new_root != state.current_root).then_some((
*shard_id,
state.current_root,
new_root,
))
})
})
.collect();
let new_seq = if let Some((refs, name, created, expected_seq)) = durable {
store
.sync_dirty_dirs()
.map_err(|error| BranchCommitError::Barrier(error.to_string()))?;
let Some(record) = refs.get(&name) else {
return Err(BranchCommitError::Ref(BranchRefError::BranchRemoved(name)));
};
let parents: Vec<Hash> = record
.shards
.iter()
.map(|shard| shard.head)
.chain(extra_parents.iter().copied())
.collect();
let new_heads: Vec<(ShardId, Hash)> = advanced
.iter()
.map(|&(shard_id, _, new_root)| (shard_id, new_root))
.collect();
Some(refs.advance(&name, created, expected_seq, &new_heads, parents, timestamp)?)
} else {
None
};
for &(_, old_root, new_root) in &advanced {
advance_in(&mut counts, old_root, new_root);
registry_guard.replace(old_root, new_root);
}
for ((_, state), new_root) in states.iter_mut().zip(&materialised) {
if let Some(new_root) = new_root {
state.current_root = *new_root;
state.buffer = WalBuffer::new();
}
}
if let (Some(bound), Some(seq)) = (binding.as_deref_mut(), new_seq) {
bound.seq = seq;
}
let heads = current_heads(&states);
drop(counts);
drop(binding);
drop(states);
Ok(CommitOutcome {
heads,
seq: new_seq,
advanced: true,
})
}
fn lock_binding(binding: &Mutex<RefBinding>) -> MutexGuard<'_, RefBinding> {
match binding.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
fn buffered_batch(buffer: &WalBuffer) -> Vec<(Vec<u8>, Option<Vec<u8>>)> {
buffer
.iter()
.map(|mutation| match mutation {
Mutation::Put { key, value } => (key.clone(), Some(value.clone())),
Mutation::Delete { key } => (key.clone(), None),
})
.collect()
}
fn current_heads(states: &[(ShardId, MutexGuard<'_, ShardState>)]) -> Vec<(ShardId, Hash)> {
states
.iter()
.map(|(shard_id, state)| (*shard_id, state.current_root))
.collect()
}
#[cfg(test)]
#[path = "commit_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "commit_error_tests.rs"]
mod error_tests;