use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
use crate::store::{DeleteNode, NodeStore};
use crate::tree::{Hash, Node};
use super::refstore::BranchRefStore;
use super::registry::BranchRegistry;
use super::snapshot::{SnapshotError, SnapshotRegistry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PruneReport {
pub node_count: usize,
pub bytes_reclaimed: usize,
}
#[derive(Debug)]
pub enum PruneError {
UnknownSnapshot { name: String },
SnapshotRegistry(SnapshotError),
MissingNode { hash: Hash },
StoreRead { hash: Hash },
NodeDelete { hash: Hash },
}
impl fmt::Display for PruneError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownSnapshot { name } => write!(formatter, "unknown snapshot: {name}"),
Self::SnapshotRegistry(error) => write!(formatter, "snapshot registry error: {error}"),
Self::MissingNode { hash } => write!(formatter, "missing tree node {hash}"),
Self::StoreRead { hash } => write!(formatter, "failed to read tree node {hash}"),
Self::NodeDelete { hash } => write!(formatter, "failed to delete tree node {hash}"),
}
}
}
impl std::error::Error for PruneError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::SnapshotRegistry(error) => Some(error),
Self::UnknownSnapshot { .. }
| Self::MissingNode { .. }
| Self::StoreRead { .. }
| Self::NodeDelete { .. } => None,
}
}
}
impl From<SnapshotError> for PruneError {
fn from(error: SnapshotError) -> Self {
Self::SnapshotRegistry(error)
}
}
pub fn prune<S>(
store: &S,
branches: &BranchRegistry,
refs: &BranchRefStore,
snapshots: &mut SnapshotRegistry,
name: &str,
) -> Result<PruneReport, PruneError>
where
S: NodeStore + DeleteNode + ?Sized,
{
let removed_root = snapshots
.get(name)
.ok_or_else(|| PruneError::UnknownSnapshot {
name: name.to_owned(),
})?;
let removed_reachable = collect_reachable(store, [removed_root])?;
let live_roots = live_roots_excluding(branches, refs, snapshots, name);
let live_reachable = collect_reachable(store, live_roots)?;
let unreferenced = unreferenced_nodes(removed_reachable, &live_reachable);
let report = PruneReport {
node_count: unreferenced.len(),
bytes_reclaimed: unreferenced.iter().map(|(_hash, bytes)| *bytes).sum(),
};
snapshots
.remove(name)?
.ok_or_else(|| PruneError::UnknownSnapshot {
name: name.to_owned(),
})?;
for (hash, _bytes) in unreferenced {
store
.delete(&hash)
.map_err(|_error| PruneError::NodeDelete { hash })?;
}
Ok(report)
}
fn live_roots_excluding(
branches: &BranchRegistry,
refs: &BranchRefStore,
snapshots: &SnapshotRegistry,
excluded: &str,
) -> HashSet<Hash> {
let mut roots = branches.live_roots();
roots.extend(refs.protected_roots());
roots.extend(
snapshots
.list_snapshots()
.into_iter()
.filter(|(name, _root_hash, _timestamp)| name != excluded)
.map(|(_name, root_hash, _timestamp)| root_hash),
);
roots
}
fn collect_reachable<S, I>(store: &S, roots: I) -> Result<HashMap<Hash, usize>, PruneError>
where
S: NodeStore + ?Sized,
I: IntoIterator<Item = Hash>,
{
let mut reachable = HashMap::new();
let mut stack: Vec<Hash> = roots.into_iter().collect();
while let Some(hash) = stack.pop() {
if reachable.contains_key(&hash) {
continue;
}
let node = load_node(store, hash)?;
let serialised_len = node.serialise().len();
if let Node::Internal(internal) = &*node {
stack.extend(
internal
.children()
.iter()
.map(|(_lower_bound, child_hash)| *child_hash),
);
}
reachable.insert(hash, serialised_len);
}
Ok(reachable)
}
fn load_node<S>(store: &S, hash: Hash) -> Result<Arc<Node>, PruneError>
where
S: NodeStore + ?Sized,
{
store
.get(&hash)
.map_err(|_error| PruneError::StoreRead { hash })?
.ok_or(PruneError::MissingNode { hash })
}
fn unreferenced_nodes(
removed_reachable: HashMap<Hash, usize>,
live_reachable: &HashMap<Hash, usize>,
) -> Vec<(Hash, usize)> {
removed_reachable
.into_iter()
.filter(|(hash, _bytes)| !live_reachable.contains_key(hash))
.collect()
}
#[cfg(test)]
#[path = "prune_tests.rs"]
mod tests;