use super::super::dir::DirCompaction;
use super::super::error::PersistError;
use super::files::DirPath;
use super::maintenance::{PruneReport, prune_estimate};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct CompactionPolicy {
pub min_garbage_ratio: f64,
pub min_reclaimable_bytes: u64,
pub min_total_bytes: u64,
}
impl Default for CompactionPolicy {
fn default() -> Self {
Self {
min_garbage_ratio: 0.5,
min_reclaimable_bytes: 64 << 20,
min_total_bytes: 64 << 20,
}
}
}
impl CompactionPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_min_garbage_ratio(mut self, ratio: f64) -> Self {
self.min_garbage_ratio = if ratio.is_nan() {
0.0
} else {
ratio.clamp(0.0, 1.0)
};
self
}
pub fn with_min_reclaimable_bytes(mut self, bytes: u64) -> Self {
self.min_reclaimable_bytes = bytes;
self
}
pub fn with_min_total_bytes(mut self, bytes: u64) -> Self {
self.min_total_bytes = bytes;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdviceRule {
ThresholdsMet,
NotWritable,
TotalBytes,
ReclaimableBytes,
GarbageRatio,
DiskSpace,
}
impl AdviceRule {
pub fn name(&self) -> &'static str {
match self {
AdviceRule::ThresholdsMet => "thresholds_met",
AdviceRule::NotWritable => "not_writable",
AdviceRule::TotalBytes => "total_bytes",
AdviceRule::ReclaimableBytes => "reclaimable_bytes",
AdviceRule::GarbageRatio => "garbage_ratio",
AdviceRule::DiskSpace => "disk_space",
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct CompactionAdvice {
pub recommended: bool,
pub decided_by: AdviceRule,
pub reason: String,
pub compactable: bool,
pub total_bytes: u64,
pub live_bytes: u64,
pub garbage_bytes: u64,
pub garbage_ratio: f64,
pub prune_bytes: u64,
pub prune_first: bool,
pub prune: PruneReport,
pub reclaimable_bytes: u64,
pub reclaimable_ratio: f64,
pub space_needed: u64,
pub free_space: Option<u64>,
pub policy: CompactionPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct CompactReport {
pub prune: PruneReport,
pub compaction: Option<DirCompaction>,
}
impl CompactReport {
pub fn bytes_freed(&self) -> u64 {
self.compaction
.map_or(0, |c| c.bytes_before.saturating_sub(c.bytes_after))
}
}
fn size_text(n: u64) -> String {
const UNITS: [&str; 4] = ["KiB", "MiB", "GiB", "TiB"];
if n < 1024 {
return format!("{n} B");
}
let mut value = n as f64 / 1024.0;
let mut unit = 0;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
format!("{value:.1} {}", UNITS[unit])
}
pub(super) fn advise(
dir: &DirPath,
policy: CompactionPolicy,
blocked: Option<String>,
) -> Result<CompactionAdvice, PersistError> {
let (prune, prune_bytes) = prune_estimate(dir, u64::MAX)?;
let backend = dir.store_dir();
let usage = backend.space_usage();
let compactable = usage.is_some();
let (total, live, garbage, needed, free) = match usage {
Some(usage) => (
usage.total_bytes,
usage.live_bytes,
usage.garbage_bytes(),
usage.live_bytes.saturating_sub(prune_bytes),
backend.free_space(),
),
None => {
let total = super::rollback::tree_bytes(dir);
(total, total, 0, 0, None)
}
};
let reclaimable = garbage.saturating_add(prune_bytes).min(total);
let ratio = |n: u64| {
if total == 0 {
0.0
} else {
n as f64 / total as f64
}
};
let reclaimable_ratio = ratio(reclaimable);
let mib = size_text;
let action = if compactable {
"a compaction"
} else {
"compact() (a prune for a directory store)"
};
let (decided_by, reason) = if let Some(why) = blocked {
(
AdviceRule::NotWritable,
format!("the store cannot change now: {why}"),
)
} else if total < policy.min_total_bytes {
(
AdviceRule::TotalBytes,
format!(
"the store is smaller than the minimum of {}",
mib(policy.min_total_bytes)
),
)
} else if reclaimable < policy.min_reclaimable_bytes.max(1) {
(
AdviceRule::ReclaimableBytes,
format!(
"{action} would give back {} (below the minimum of {})",
mib(reclaimable),
mib(policy.min_reclaimable_bytes)
),
)
} else if reclaimable_ratio < policy.min_garbage_ratio {
(
AdviceRule::GarbageRatio,
format!(
"{action} would give back {:.0} % of the store (below the minimum of {:.0} %)",
reclaimable_ratio * 100.0,
policy.min_garbage_ratio * 100.0
),
)
} else if compactable && free.is_some_and(|free| free < needed) {
(
AdviceRule::DiskSpace,
format!(
"the new file needs about {} of free disk space, {} are free",
mib(needed),
mib(free.unwrap_or(0))
),
)
} else {
(
AdviceRule::ThresholdsMet,
format!(
"{action} would give back {} ({:.0} % of the store){}",
mib(reclaimable),
reclaimable_ratio * 100.0,
if prune_bytes > 0 {
format!(
", {} of it by pruning old snapshots and WAL",
mib(prune_bytes)
)
} else {
String::new()
}
),
)
};
Ok(CompactionAdvice {
recommended: decided_by == AdviceRule::ThresholdsMet,
decided_by,
reason,
compactable,
total_bytes: total,
live_bytes: live,
garbage_bytes: garbage,
garbage_ratio: ratio(garbage),
prune_bytes,
prune_first: prune_bytes > 0,
prune,
reclaimable_bytes: reclaimable,
reclaimable_ratio,
space_needed: needed,
free_space: free,
policy,
})
}