use crate::{
compactions_store::CompactionsStore, config::GarbageCollectorDirectoryOptions,
error::SlateDBError,
};
use chrono::{DateTime, Utc};
use futures::StreamExt;
use log::error;
use std::sync::Arc;
use super::filter::retain_allowed_by_gc_filter;
use super::{GcFilter, GcStats, GcTask, GC_DELETE_CONCURRENCY};
#[derive(Clone)]
pub(crate) struct CompactionsGcTask {
compactions_store: Arc<CompactionsStore>,
stats: Arc<GcStats>,
compactions_options: GarbageCollectorDirectoryOptions,
gc_filter: Option<Arc<dyn GcFilter>>,
boundary_files_enabled: bool,
}
impl std::fmt::Debug for CompactionsGcTask {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompactionsGcTask")
.field("compactions_options", &self.compactions_options)
.field("boundary_files_enabled", &self.boundary_files_enabled)
.finish()
}
}
impl CompactionsGcTask {
pub(super) fn new(
compactions_store: Arc<CompactionsStore>,
stats: Arc<GcStats>,
compactions_options: GarbageCollectorDirectoryOptions,
gc_filter: Option<Arc<dyn GcFilter>>,
boundary_files_enabled: bool,
) -> Self {
Self {
compactions_store,
stats,
compactions_options,
gc_filter,
boundary_files_enabled,
}
}
fn compactions_min_age(&self) -> chrono::Duration {
chrono::Duration::from_std(self.compactions_options.min_age).expect("invalid duration")
}
async fn maybe_delete_compactions(&self, compactions_ids: Vec<u64>) {
if self.compactions_options.dry_run {
if !compactions_ids.is_empty() {
log::info!(
"dry run: skipping compactions deletion [count={}]",
compactions_ids.len()
);
}
for id in compactions_ids {
log::debug!(
"dry run: would delete compactions but skipped [id={:?}]",
id
);
}
return;
}
futures::stream::iter(compactions_ids)
.for_each_concurrent(GC_DELETE_CONCURRENCY, |id| async move {
if let Err(e) = self
.compactions_store
.delete_compactions_unchecked(id)
.await
{
error!("error deleting compactions [id={:?}, error={}]", id, e);
} else {
self.stats.gc_compactions_count.increment(1);
}
})
.await;
}
}
impl GcTask for CompactionsGcTask {
async fn collect(&self, utc_now: DateTime<Utc>) -> Result<(), SlateDBError> {
let min_age = self.compactions_min_age();
let mut compactions_metadata_list = self.compactions_store.list_compactions(..).await?;
compactions_metadata_list.pop();
let compactions_to_delete = compactions_metadata_list
.into_iter()
.filter(|compactions_metadata| {
utc_now.signed_duration_since(compactions_metadata.metadata.last_modified) > min_age
})
.collect::<Vec<_>>();
if self.boundary_files_enabled {
if let Some(boundary) = compactions_to_delete
.iter()
.map(|compactions_metadata| compactions_metadata.id)
.max()
{
self.compactions_store.advance_boundary(boundary).await?;
}
}
let compactions_to_delete =
retain_allowed_by_gc_filter(&self.gc_filter, compactions_to_delete).await;
let compactions_ids_to_delete = compactions_to_delete
.into_iter()
.map(|compactions_metadata| compactions_metadata.id)
.collect::<Vec<_>>();
self.maybe_delete_compactions(compactions_ids_to_delete)
.await;
Ok(())
}
fn resource(&self) -> &str {
"Compactions"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compactions_store::{CompactionsStore, StoredCompactions};
use async_trait::async_trait;
use chrono::TimeDelta;
use object_store::{memory::InMemory, path::Path, ObjectStoreExt};
use slatedb_common::metrics::MetricsRecorderHelper;
use slatedb_common::ObjectMetadata;
use std::collections::HashSet;
use std::time::Duration;
struct DenyAllGcFilter;
#[async_trait]
impl GcFilter for DenyAllGcFilter {
async fn filter(&self, _candidates: HashSet<ObjectMetadata>) -> HashSet<ObjectMetadata> {
HashSet::new()
}
}
#[tokio::test]
async fn test_collect_advances_boundary_for_old_compactions_files() {
let object_store = Arc::new(InMemory::new());
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
object_store.clone(),
));
let mut stored_compactions = StoredCompactions::create(compactions_store.clone(), 0)
.await
.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
let recorder = MetricsRecorderHelper::noop();
let task = CompactionsGcTask::new(
compactions_store.clone(),
Arc::new(GcStats::new(&recorder)),
GarbageCollectorDirectoryOptions {
min_age: Duration::from_secs(1),
interval: None,
dry_run: false,
},
None,
true,
);
task.collect(Utc::now() + TimeDelta::hours(1))
.await
.unwrap();
let raw_boundary = object_store
.get(&Path::from("/root/gc/compactions.boundary"))
.await
.unwrap()
.bytes()
.await
.unwrap();
assert_eq!("2", std::str::from_utf8(&raw_boundary).unwrap());
let compactions = compactions_store.list_compactions(..).await.unwrap();
assert_eq!(
vec![3],
compactions
.iter()
.map(|compactions| compactions.id)
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_collect_without_boundary_advancement_deletes_and_preserves_boundary() {
let object_store = Arc::new(InMemory::new());
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
object_store.clone(),
));
let mut stored_compactions = StoredCompactions::create(compactions_store.clone(), 0)
.await
.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
compactions_store.advance_boundary(1).await.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
let recorder = MetricsRecorderHelper::noop();
let task = CompactionsGcTask::new(
compactions_store.clone(),
Arc::new(GcStats::new(&recorder)),
GarbageCollectorDirectoryOptions {
min_age: Duration::from_secs(1),
interval: None,
dry_run: false,
},
None,
false,
);
task.collect(Utc::now() + TimeDelta::hours(1))
.await
.unwrap();
let raw_boundary = object_store
.get(&Path::from("/root/gc/compactions.boundary"))
.await
.unwrap()
.bytes()
.await
.unwrap();
assert_eq!("1", std::str::from_utf8(&raw_boundary).unwrap());
let compactions = compactions_store.list_compactions(..).await.unwrap();
assert_eq!(
vec![3],
compactions
.iter()
.map(|compactions| compactions.id)
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_collect_advances_boundary_before_filtering_compactions_files() {
let object_store = Arc::new(InMemory::new());
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
object_store.clone(),
));
let mut stored_compactions = StoredCompactions::create(compactions_store.clone(), 0)
.await
.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
stored_compactions
.update(stored_compactions.prepare_dirty().unwrap())
.await
.unwrap();
let recorder = MetricsRecorderHelper::noop();
let task = CompactionsGcTask::new(
compactions_store.clone(),
Arc::new(GcStats::new(&recorder)),
GarbageCollectorDirectoryOptions {
min_age: Duration::from_secs(1),
interval: None,
dry_run: false,
},
Some(Arc::new(DenyAllGcFilter) as Arc<dyn GcFilter>),
true,
);
task.collect(Utc::now() + TimeDelta::hours(1))
.await
.unwrap();
let raw_boundary = object_store
.get(&Path::from("/root/gc/compactions.boundary"))
.await
.unwrap()
.bytes()
.await
.unwrap();
assert_eq!("2", std::str::from_utf8(&raw_boundary).unwrap());
assert!(compactions_store
.try_read_compactions(1)
.await
.unwrap()
.is_some());
assert!(compactions_store
.try_read_compactions(2)
.await
.unwrap()
.is_some());
}
}