use super::cache::{GcActionStoreReport, GcReport, GcTargetReport, print_json};
use crate::config::{Config, RetentionSettings};
use crate::{store, target};
use bytesize::ByteSize;
use eyre::Result;
use std::path::Path;
#[derive(usage::Args)]
pub(super) struct GcArgs {
#[usage(long, value_name = "SIZE")]
pub(super) max_size: Option<ByteSize>,
#[usage(long)]
pub(super) json: bool,
#[usage(long)]
pub(super) dry_run: bool,
}
pub(super) fn run(
config: &Config,
max_bytes: u64,
dry_run: bool,
json: bool,
retention: &RetentionSettings,
) -> Result<()> {
let store = config.store_dir();
let target_budget = target_budget(retention, max_bytes);
let pruned = target::collect(
&config.target.root,
target_budget,
retention.target_max_age,
dry_run,
);
let incremental = match crate::incremental::prune(
&config.cache_dir.join("incremental"),
retention.target_max_age,
dry_run,
) {
Ok(outcome) => outcome,
Err(error) => {
log::warn!("learned incremental state was not collected: {error}");
crate::incremental::PruneOutcome::default()
}
};
let projected_target_bytes = match &pruned {
Ok(outcome) => outcome.remaining_bytes,
Err(_) if retention.max_total_bytes.is_some() => target::stats(&config.target.root)
.map(|stats| stats.bytes)
.unwrap_or_default(),
Err(_) => 0,
};
let store_budget = retention.max_total_bytes.map_or(max_bytes, |total| {
max_bytes.min(total.saturating_sub(projected_target_bytes))
});
if !dry_run {
crate::scheduler::prune_flights(&config.cache_dir);
}
let outcome = if dry_run {
store::gc_dry_run(&store, store_budget)
} else {
store::gc(&store, store_budget)
};
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
let mut freed_bytes = incremental.removed_bytes;
match pruned {
Ok(pruned) => {
freed_bytes = freed_bytes.saturating_add(pruned.removed_bytes);
if !json && pruned.removed_views > 0 {
println!("{}", target_removals(&pruned, dry_run));
}
}
Err(prune_error) => {
log::warn!("target directories were not collected: {prune_error}");
}
}
record_collection(&store, 0, freed_bytes, dry_run);
if !json {
print_incremental_removals(&incremental, dry_run);
}
return Err(error);
}
};
record_collection(
&store,
outcome.removed_bytes,
pruned.as_ref().map_or(0, |pruned| pruned.removed_bytes) + incremental.removed_bytes,
dry_run,
);
if json {
let pruned = pruned?;
print_json(&GcReport {
version: 1,
max_bytes,
dry_run,
action_store: GcActionStoreReport {
removed_objects: outcome.removed_objects,
removed_action_results: outcome.removed_action_results,
removed_checkout_records: outcome.removed_checkout_records,
removed_session_streams: outcome.removed_session_streams,
removed_bytes: outcome.removed_bytes,
remaining_bytes: outcome.remaining_bytes,
},
targets: GcTargetReport {
removed_directories: pruned.removed_views,
removed_bytes: pruned.removed_bytes,
},
})?;
} else {
print_gc_store_outcome(&outcome, dry_run);
print_incremental_removals(&incremental, dry_run);
let pruned = pruned?;
if pruned.removed_views > 0 {
println!("{}", target_removals(&pruned, dry_run));
}
}
Ok(())
}
fn print_incremental_removals(outcome: &crate::incremental::PruneOutcome, dry_run: bool) {
if outcome.removed_directories == 0 {
return;
}
let verb = if dry_run { "would free" } else { "freed" };
println!(
"{verb} {} learned incremental directories ({})",
outcome.removed_directories,
ByteSize::b(outcome.removed_bytes).display().iec()
);
}
pub(super) fn record_collection(store: &Path, store_bytes: u64, target_bytes: u64, dry_run: bool) {
if dry_run || (store_bytes == 0 && target_bytes == 0) {
return;
}
crate::savings::record_quietly(
store,
&crate::savings::Delta {
freed_store_bytes: store_bytes,
freed_target_bytes: target_bytes,
..crate::savings::Delta::default()
},
);
}
pub(super) fn print_gc_store_outcome(outcome: &store::GcOutcome, dry_run: bool) {
let prefix = if dry_run { "would have " } else { "" };
println!("{prefix}{}", evictions(outcome));
if outcome.removed_checkout_records > 0 {
println!(
"{prefix}dropped {} stale checkout records",
outcome.removed_checkout_records
);
}
if outcome.removed_session_streams > 0 {
println!(
"{prefix}dropped {} session event streams",
outcome.removed_session_streams
);
}
}
pub(super) fn target_removals(outcome: &target::CollectionOutcome, dry_run: bool) -> String {
let verb = if dry_run { "would free" } else { "freed" };
format!(
"{verb} {} target directories ({}, {} abandoned and {} live); {} remain",
outcome.removed_views,
ByteSize::b(outcome.removed_bytes).display().iec(),
outcome.removed_stale_views,
outcome.removed_live_views,
ByteSize::b(outcome.remaining_bytes).display().iec(),
)
}
pub(super) fn evictions(outcome: &store::GcOutcome) -> String {
format!(
"evicted {} objects and {} action results ({}); {} remain",
outcome.removed_objects,
outcome.removed_action_results,
ByteSize::b(outcome.removed_bytes).display().iec(),
ByteSize::b(outcome.remaining_bytes).display().iec(),
)
}
pub(super) fn sweep_store(config: &Config, retention: &RetentionSettings) -> crate::savings::Delta {
let mut delta = crate::savings::Delta::default();
if !config.gc.auto {
return delta;
}
match store::claim_sweep(&config.store_dir(), config.gc.interval) {
Ok(false) => {}
Ok(true) => {
let pruned = prune_targets(config, retention, config.gc.max_bytes);
delta.freed_target_bytes = pruned.freed_bytes;
let store_budget = retention
.max_total_bytes
.map_or(config.gc.max_bytes, |total| {
let target_bytes = pruned.remaining_bytes.unwrap_or_else(|| {
target::stats(&config.target.root)
.map(|stats| stats.bytes)
.unwrap_or_default()
});
config.gc.max_bytes.min(total.saturating_sub(target_bytes))
});
crate::scheduler::prune_flights(&config.cache_dir);
let outcome = match store::gc(&config.store_dir(), store_budget) {
Ok(outcome) => outcome,
Err(error) => {
log::warn!("the store was not swept: {error}");
return delta;
}
};
delta.freed_store_bytes = outcome.removed_bytes;
if outcome.removed_bytes > 0 {
crate::session::note(&format!("mbx[gc]: {}", evictions(&outcome)));
}
}
Err(error) => {
log::warn!("the store was not swept: {error}");
delta.freed_target_bytes =
prune_targets(config, retention, config.gc.max_bytes).freed_bytes;
}
}
delta
}
pub(super) struct PruneReport {
remaining_bytes: Option<u64>,
freed_bytes: u64,
}
pub(super) fn prune_targets(
config: &Config,
retention: &RetentionSettings,
store_reserve: u64,
) -> PruneReport {
let target_budget = target_budget(retention, store_reserve);
let incremental = crate::incremental::prune(
&config.cache_dir.join("incremental"),
retention.target_max_age,
false,
);
let incremental_bytes = match incremental {
Ok(outcome) => outcome.removed_bytes,
Err(error) => {
log::warn!("learned incremental state was not collected: {error}");
0
}
};
match target::collect(
&config.target.root,
target_budget,
retention.target_max_age,
false,
) {
Ok(pruned) => {
if pruned.removed_views > 0 {
crate::session::note(&format!("mbx[gc]: {}", target_removals(&pruned, false)));
}
PruneReport {
remaining_bytes: Some(pruned.remaining_bytes),
freed_bytes: pruned.removed_bytes.saturating_add(incremental_bytes),
}
}
Err(error) => {
log::warn!("target directories were not collected: {error}");
PruneReport {
remaining_bytes: None,
freed_bytes: incremental_bytes,
}
}
}
}
pub(super) fn target_budget(retention: &RetentionSettings, store_reserve: u64) -> Option<u64> {
retention
.max_total_bytes
.map_or(retention.target_max_bytes, |total| {
let combined = total.saturating_sub(store_reserve);
Some(
retention
.target_max_bytes
.map_or(combined, |target| target.min(combined)),
)
})
}