use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use object_store::ObjectStoreExt;
use tracing::{debug, info, warn};
use super::MirrorError;
use super::layout::{self, ManifestCache, ManifestObjects, object_path};
use super::pass::pin_name;
use crate::config::ResolvedMirrorTarget;
use crate::services::DatabaseHandle;
#[derive(Clone, Copy, Debug, Default)]
pub struct PruneReport {
pub kept_manifests: u64,
pub deleted_manifests: u64,
pub deleted_objects: u64,
pub data_deletion_ran: bool,
}
pub async fn prune(
source: &DatabaseHandle,
dest: &DatabaseHandle,
target_name: &str,
settings: &ResolvedMirrorTarget,
) -> Result<PruneReport, MirrorError> {
prune_at(source, dest, target_name, settings, Utc::now()).await
}
pub async fn prune_at(
source: &DatabaseHandle,
dest: &DatabaseHandle,
target_name: &str,
settings: &ResolvedMirrorTarget,
now: DateTime<Utc>,
) -> Result<PruneReport, MirrorError> {
let Some(keep) = settings.keep else {
return Ok(PruneReport::default());
};
let keep = chrono::Duration::from_std(keep).expect("keep fits");
let min_age = chrono::Duration::from_std(settings.min_age).expect("min_age fits");
let manifests = layout::list_manifests(dest).await?;
let Some(&(latest, _)) = manifests.last() else {
return Ok(PruneReport::default());
};
let compacted = layout::list_compacted(dest).await?;
let wals = layout::list_wals(dest).await?;
let spared = source_closure(source).await;
let floor = match &spared {
Ok(_) => match source
.admin
.list_checkpoints(Some(&pin_name(target_name)))
.await
{
Ok(pins) => pins.iter().map(|cp| cp.create_time - min_age).min(),
Err(e) => {
warn!(target = target_name, "cannot list pin checkpoints: {e}");
None
}
},
Err(_) => None,
};
if let Err(e) = &spared {
warn!(
target = target_name,
"source unreachable ({e}); skipping data-object deletion"
);
}
let restore_points: Vec<u64> = manifests
.iter()
.filter(|(id, meta)| *id == latest || now - meta.last_modified < keep)
.map(|(id, _)| *id)
.collect();
let dest_ids: BTreeSet<u64> = manifests.iter().map(|(id, _)| *id).collect();
let mut kept: BTreeSet<u64> = BTreeSet::new();
let mut decoded = ManifestCache::default();
for &id in &restore_points {
kept.insert(id);
let Some(manifest) = decoded.read(dest, id).await? else {
continue;
};
let pins: Vec<u64> = manifest
.checkpoints()
.iter()
.filter(|cp| layout::checkpoint_live(cp, now))
.map(|cp| cp.manifest_id)
.collect();
for pinned in pins {
if dest_ids.contains(&pinned) {
kept.insert(pinned);
}
}
}
let mut kept_objects = ManifestObjects::default();
let mut kept_decoded = true;
for &id in kept.clone().iter() {
match decoded.read(dest, id).await? {
Some(manifest) => kept_objects.extend(&layout::manifest_objects(manifest)),
None => {
warn!(
target = target_name,
manifest = id,
"kept manifest unreadable"
);
kept_decoded = false;
}
}
}
let tail_floor = decoded
.read(dest, latest)
.await?
.map(|m| m.next_wal_sst_id())
.unwrap_or(0);
let mut report = PruneReport {
kept_manifests: kept.len() as u64,
data_deletion_ran: spared.is_ok() && kept_decoded,
..Default::default()
};
for (id, _) in &manifests {
if kept.contains(id) {
continue;
}
match dest
.store
.delete(&object_path(dest, &layout::manifest_rel(*id)))
.await
{
Ok(()) | Err(object_store::Error::NotFound { .. }) => report.deleted_manifests += 1,
Err(e) => return Err(e.into()),
}
}
let Ok(spared) = spared else {
info!(
target = target_name,
deleted_manifests = report.deleted_manifests,
"pruned manifests only (source unreachable)"
);
return Ok(report);
};
if !kept_decoded {
info!(
target = target_name,
deleted_manifests = report.deleted_manifests,
"pruned manifests only (a kept manifest is unreadable)"
);
return Ok(report);
}
let guarded =
|modified: DateTime<Utc>| now - modified <= min_age || floor.is_some_and(|f| modified >= f);
for (ulid, meta) in &compacted {
if kept_objects.compacted.contains(ulid)
|| spared.compacted.contains(ulid)
|| guarded(meta.last_modified)
{
continue;
}
match dest.store.delete(&meta.location).await {
Ok(()) | Err(object_store::Error::NotFound { .. }) => report.deleted_objects += 1,
Err(e) => return Err(e.into()),
}
debug!(object = %meta.location, "pruned");
}
for (id, meta) in &wals {
if *id >= tail_floor
|| kept_objects.wal.contains(id)
|| spared.wal.contains(id)
|| guarded(meta.last_modified)
{
continue;
}
match dest.store.delete(&meta.location).await {
Ok(()) | Err(object_store::Error::NotFound { .. }) => report.deleted_objects += 1,
Err(e) => return Err(e.into()),
}
debug!(object = %meta.location, "pruned");
}
info!(
target = target_name,
kept = report.kept_manifests,
deleted_manifests = report.deleted_manifests,
deleted_objects = report.deleted_objects,
"pruned"
);
Ok(report)
}
async fn source_closure(source: &DatabaseHandle) -> Result<ManifestObjects, MirrorError> {
let latest =
source
.admin
.read_manifest(None)
.await?
.ok_or_else(|| MirrorError::NotADatabase {
url: source.url.clone(),
})?;
let mut objects = layout::manifest_objects(&latest);
let mut seen = BTreeSet::new();
for cp in latest.checkpoints() {
if cp.manifest_id == latest.id() || !seen.insert(cp.manifest_id) {
continue;
}
if let Some(pinned) = source.admin.read_manifest(Some(cp.manifest_id)).await? {
objects.extend(&layout::manifest_objects(&pinned));
}
}
Ok(objects)
}