use crate::{
compactions_store::CompactionsStore,
compactor_state::VersionedCompactions,
compactor_state_protocols::CompactorStateReader,
config::GarbageCollectorDirectoryOptions,
db_state::SsTableId,
error::SlateDBError,
manifest::{store::ManifestStore, Manifest, VersionedManifest},
tablestore::TableStore,
};
use chrono::{DateTime, Utc};
use log::error;
use std::collections::HashSet;
use std::sync::Arc;
use super::filter::retain_allowed_by_gc_filter;
use super::{GcFilter, GcStats, GcTask};
#[derive(Clone)]
pub(crate) struct CompactedGcTask {
manifest_store: Arc<ManifestStore>,
compactions_store: Arc<CompactionsStore>,
table_store: Arc<TableStore>,
stats: Arc<GcStats>,
compacted_options: GarbageCollectorDirectoryOptions,
gc_filter: Option<Arc<dyn GcFilter>>,
}
impl std::fmt::Debug for CompactedGcTask {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompactedGcTask")
.field("compacted_options", &self.compacted_options)
.finish()
}
}
impl CompactedGcTask {
pub(super) fn new(
manifest_store: Arc<ManifestStore>,
compactions_store: Arc<CompactionsStore>,
table_store: Arc<TableStore>,
stats: Arc<GcStats>,
compacted_options: GarbageCollectorDirectoryOptions,
gc_filter: Option<Arc<dyn GcFilter>>,
) -> Self {
CompactedGcTask {
manifest_store,
compactions_store,
table_store,
stats,
compacted_options,
gc_filter,
}
}
fn compacted_sst_min_age(&self) -> chrono::Duration {
chrono::Duration::from_std(self.compacted_options.min_age).expect("invalid duration")
}
async fn list_active_l0_and_compacted_ssts(
&self,
manifest_id: u64,
manifest: &Manifest,
) -> Result<HashSet<SsTableId>, SlateDBError> {
let active_manifests = self
.manifest_store
.read_referenced_manifests(manifest_id, manifest)
.await?;
Ok(collect_active_ssts(active_manifests.values()))
}
fn compaction_low_watermark_dt(compactions: &Option<VersionedCompactions>) -> DateTime<Utc> {
match compactions {
Some(compactions) => compactions
.compactions
.iter()
.map(|c| DateTime::<Utc>::from(c.id().datetime()))
.min()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH),
None => DateTime::<Utc>::UNIX_EPOCH,
}
}
}
fn collect_active_ssts<'a>(manifests: impl Iterator<Item = &'a Manifest>) -> HashSet<SsTableId> {
let mut active = HashSet::new();
for manifest in manifests {
for tree in manifest.core.trees() {
for view in tree.l0.iter() {
active.insert(view.sst.id);
}
for sr in tree.compacted.iter() {
for view in sr.sst_views.iter() {
active.insert(view.sst.id);
}
}
}
}
active
}
fn newest_l0_dt(manifest: &Manifest) -> DateTime<Utc> {
manifest
.core
.trees()
.filter_map(|tree| {
if !tree.l0.is_empty() {
tree.l0
.iter()
.map(|view| DateTime::<Utc>::from(view.sst.id.unwrap_compacted_id().datetime()))
.max()
} else {
tree.last_compacted_l0_sst_view_id
.map(|id| DateTime::<Utc>::from(id.datetime()))
}
})
.max()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH)
}
impl GcTask for CompactedGcTask {
async fn collect(&self, utc_now: DateTime<Utc>) -> Result<(), SlateDBError> {
let state_reader = CompactorStateReader::new(&self.manifest_store, &self.compactions_store);
let view = state_reader.read_view().await?;
let compactions = view.compactions;
let VersionedManifest {
id: manifest_id,
manifest,
} = view.manifest;
let compaction_low_watermark_dt = Self::compaction_low_watermark_dt(&compactions);
let active_ssts = self
.list_active_l0_and_compacted_ssts(manifest_id, &manifest)
.await?;
let configured_min_age_dt = utc_now - self.compacted_sst_min_age();
let newest_l0_dt = newest_l0_dt(&manifest);
let cutoff_dt = configured_min_age_dt
.min(compaction_low_watermark_dt)
.min(newest_l0_dt);
log::debug!(
"calculated compacted SST GC cutoff [cutoff_dt={:?}, configured_min_age_dt={:?}, compaction_low_watermark_dt={:?}, most_recent_sst_dt={:?}]",
cutoff_dt,
configured_min_age_dt,
compaction_low_watermark_dt,
newest_l0_dt,
);
let ssts_to_delete = self
.table_store
.list_compacted_ssts(..)
.await?
.into_iter()
.filter(|sst| {
DateTime::<Utc>::from(sst.id.unwrap_compacted_id().datetime()) < cutoff_dt
})
.filter(|sst| !active_ssts.contains(&sst.id))
.collect::<Vec<_>>();
let ssts_to_delete = retain_allowed_by_gc_filter(&self.gc_filter, ssts_to_delete).await;
let sst_ids_to_delete = ssts_to_delete
.into_iter()
.map(|sst| sst.id)
.collect::<Vec<_>>();
if self.compacted_options.dry_run && !sst_ids_to_delete.is_empty() {
log::info!(
"dry run: skipping SST deletion [count={}]",
sst_ids_to_delete.len()
);
}
for id in sst_ids_to_delete {
if self.compacted_options.dry_run {
log::debug!("dry run: would delete SST but skipped [id={:?}]", id);
continue;
}
log::info!("deleting SST [id={:?}]", id);
if let Err(e) = self.table_store.delete_sst(&id).await {
error!("error deleting SST [id={:?}, error={}]", id, e);
} else {
self.stats.gc_compacted_count.increment(1);
}
}
Ok(())
}
fn resource(&self) -> &str {
"Compacted SSTs"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compactions_store::{CompactionsStore, StoredCompactions};
use crate::compactor_state::{Compaction, CompactionSpec, SourceId};
use crate::db_state::{SortedRun, SsTableHandle, SsTableId, SsTableInfo, SsTableView};
use crate::format::sst::{SsTableFormat, SST_FORMAT_VERSION_LATEST};
use crate::manifest::store::StoredManifest;
use crate::manifest::{LsmTreeState, Manifest, ManifestCore, Segment};
use crate::object_stores::ObjectStores;
use crate::tablestore::TableStoreKind;
use crate::test_utils::build_test_sst;
use bytes::Bytes;
use object_store::{memory::InMemory, path::Path};
use slatedb_common::clock::DefaultSystemClock;
use std::collections::{BTreeMap, VecDeque};
use std::time::Duration;
#[tokio::test]
async fn test_compacted_gc_respects_min_age_cutoff() {
let main_store = Arc::new(InMemory::new());
let object_stores = ObjectStores::new(main_store.clone(), None);
let format = SsTableFormat::default();
let table_store = Arc::new(TableStore::new(
object_stores,
format.clone(),
Path::from("/root"),
None,
TableStoreKind::GC,
));
let manifest_store = Arc::new(ManifestStore::new(&Path::from("/root"), main_store.clone()));
let mut stored_manifest = StoredManifest::create_new_db(
manifest_store.clone(),
ManifestCore::new(),
Arc::new(DefaultSystemClock::new()),
)
.await
.unwrap();
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
main_store.clone(),
));
let mut stored_compactions = StoredCompactions::create(
compactions_store.clone(),
stored_manifest.manifest().compactor_epoch,
)
.await
.unwrap();
let mut compactions_dirty = stored_compactions.prepare_dirty().unwrap();
compactions_dirty.value.insert(Compaction::new(
ulid::Ulid::from_parts(9_000, 0),
CompactionSpec::new(vec![SourceId::SortedRun(0)], 0),
));
stored_compactions.update(compactions_dirty).await.unwrap();
let id_to_delete = SsTableId::Compacted(ulid::Ulid::from_parts(1_000, 0));
let id_within_min_age = SsTableId::Compacted(ulid::Ulid::from_parts(7_000, 0));
let id_active_recent = SsTableId::Compacted(ulid::Ulid::from_parts(8_000, 0));
let sst_to_delete = build_test_sst(&format, 1).await;
let sst_within_min_age = build_test_sst(&format, 1).await;
let sst_active_recent = build_test_sst(&format, 1).await;
table_store
.write_sst(&id_to_delete, &sst_to_delete, false)
.await
.unwrap();
table_store
.write_sst(&id_within_min_age, &sst_within_min_age, false)
.await
.unwrap();
let active_handle = table_store
.write_sst(&id_active_recent, &sst_active_recent, false)
.await
.unwrap();
let mut dirty = stored_manifest.prepare_dirty().unwrap();
Arc::make_mut(&mut dirty.value.core.tree)
.l0
.push_back(SsTableView::identity(active_handle));
stored_manifest.update(dirty).await.unwrap();
let recorder = slatedb_common::metrics::MetricsRecorderHelper::noop();
let opts = GarbageCollectorDirectoryOptions {
interval: None,
min_age: Duration::from_secs(5),
dry_run: false,
};
let stats = Arc::new(GcStats::new(&recorder));
let task = CompactedGcTask::new(
manifest_store.clone(),
compactions_store.clone(),
table_store.clone(),
stats,
opts,
None,
);
let utc_now = DateTime::<Utc>::from_timestamp_millis(10_000).unwrap();
task.collect(utc_now).await.unwrap();
let remaining: Vec<_> = table_store
.list_compacted_ssts(..)
.await
.unwrap()
.into_iter()
.map(|m| m.id)
.collect();
assert_eq!(remaining, vec![id_within_min_age, id_active_recent]);
}
#[tokio::test]
async fn test_compacted_gc_respects_manifest_most_recent_sst() {
let main_store = Arc::new(InMemory::new());
let object_stores = ObjectStores::new(main_store.clone(), None);
let format = SsTableFormat::default();
let table_store = Arc::new(TableStore::new(
object_stores,
format.clone(),
Path::from("/root"),
None,
TableStoreKind::GC,
));
let manifest_store = Arc::new(ManifestStore::new(&Path::from("/root"), main_store.clone()));
let mut stored_manifest = StoredManifest::create_new_db(
manifest_store.clone(),
ManifestCore::new(),
Arc::new(DefaultSystemClock::new()),
)
.await
.unwrap();
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
main_store.clone(),
));
let mut stored_compactions = StoredCompactions::create(
compactions_store.clone(),
stored_manifest.manifest().compactor_epoch,
)
.await
.unwrap();
let mut compactions_dirty = stored_compactions.prepare_dirty().unwrap();
compactions_dirty.value.insert(Compaction::new(
ulid::Ulid::from_parts(5_000, 0),
CompactionSpec::new(vec![SourceId::SortedRun(0)], 0),
));
stored_compactions.update(compactions_dirty).await.unwrap();
let id_to_delete = SsTableId::Compacted(ulid::Ulid::from_parts(1_000, 0));
let id_manifest = SsTableId::Compacted(ulid::Ulid::from_parts(3_000, 0));
let id_newer = SsTableId::Compacted(ulid::Ulid::from_parts(4_000, 0));
let sst_to_delete = build_test_sst(&format, 1).await;
let sst_manifest = build_test_sst(&format, 1).await;
let sst_newer = build_test_sst(&format, 1).await;
table_store
.write_sst(&id_to_delete, &sst_to_delete, false)
.await
.unwrap();
let manifest_handle = table_store
.write_sst(&id_manifest, &sst_manifest, false)
.await
.unwrap();
table_store
.write_sst(&id_newer, &sst_newer, false)
.await
.unwrap();
let mut dirty = stored_manifest.prepare_dirty().unwrap();
Arc::make_mut(&mut dirty.value.core.tree)
.l0
.push_back(SsTableView::identity(manifest_handle));
stored_manifest.update(dirty).await.unwrap();
let recorder = slatedb_common::metrics::MetricsRecorderHelper::noop();
let opts = GarbageCollectorDirectoryOptions {
interval: None,
min_age: Duration::from_secs(0),
dry_run: false,
};
let stats = Arc::new(GcStats::new(&recorder));
let task = CompactedGcTask::new(
manifest_store.clone(),
compactions_store.clone(),
table_store.clone(),
stats,
opts,
None,
);
let utc_now = DateTime::<Utc>::from_timestamp_millis(10_000).unwrap();
task.collect(utc_now).await.unwrap();
let remaining: Vec<_> = table_store
.list_compacted_ssts(..)
.await
.unwrap()
.into_iter()
.map(|m| m.id)
.collect();
assert_eq!(remaining, vec![id_manifest, id_newer]);
}
#[tokio::test]
async fn test_compacted_gc_respects_compaction_barrier() {
let main_store = Arc::new(InMemory::new());
let object_stores = ObjectStores::new(main_store.clone(), None);
let format = SsTableFormat::default();
let table_store = Arc::new(TableStore::new(
object_stores,
format.clone(),
Path::from("/root"),
None,
TableStoreKind::GC,
));
let manifest_store = Arc::new(ManifestStore::new(&Path::from("/root"), main_store.clone()));
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
main_store.clone(),
));
let mut stored_manifest = StoredManifest::create_new_db(
manifest_store.clone(),
ManifestCore::new(),
Arc::new(DefaultSystemClock::new()),
)
.await
.unwrap();
let compactor_epoch = stored_manifest.manifest().compactor_epoch;
let id_to_delete = SsTableId::Compacted(ulid::Ulid::from_parts(1_000, 0)); let id_barrier = SsTableId::Compacted(ulid::Ulid::from_parts(2_000, 0)); let id_to_newer = SsTableId::Compacted(ulid::Ulid::from_parts(3_000, 0)); let sst_to_delete = build_test_sst(&format, 1).await;
let sst_barrier = build_test_sst(&format, 1).await;
let sst_to_newer = build_test_sst(&format, 1).await;
table_store
.write_sst(&id_to_delete, &sst_to_delete, false)
.await
.unwrap();
table_store
.write_sst(&id_barrier, &sst_barrier, false)
.await
.unwrap();
let active_handle = table_store
.write_sst(&id_to_newer, &sst_to_newer, false)
.await
.unwrap();
let mut dirty = stored_manifest.prepare_dirty().unwrap();
Arc::make_mut(&mut dirty.value.core.tree)
.l0
.push_back(SsTableView::identity(active_handle));
stored_manifest.update(dirty).await.unwrap();
let mut stored_compactions =
StoredCompactions::create(compactions_store.clone(), compactor_epoch)
.await
.unwrap();
let mut compactions_dirty = stored_compactions.prepare_dirty().unwrap();
compactions_dirty.value.insert(Compaction::new(
ulid::Ulid::from_parts(2_000, 0),
CompactionSpec::new(vec![SourceId::SortedRun(0)], 0),
));
stored_compactions.update(compactions_dirty).await.unwrap();
let opts = GarbageCollectorDirectoryOptions {
interval: None,
min_age: Duration::from_secs(0),
dry_run: false,
};
let recorder = slatedb_common::metrics::MetricsRecorderHelper::noop();
let stats = Arc::new(GcStats::new(&recorder));
let task = CompactedGcTask::new(
manifest_store.clone(),
compactions_store.clone(),
table_store.clone(),
stats,
opts,
None,
);
let utc_now = DateTime::<Utc>::from_timestamp_millis(10_000).unwrap();
task.collect(utc_now).await.unwrap();
let remaining: Vec<_> = table_store
.list_compacted_ssts(..)
.await
.unwrap()
.into_iter()
.map(|m| m.id)
.collect();
assert_eq!(remaining, vec![id_barrier, id_to_newer]);
}
#[tokio::test]
async fn test_compacted_gc_skips_running_compaction_output_without_watermark() {
let main_store = Arc::new(InMemory::new());
let object_stores = ObjectStores::new(main_store.clone(), None);
let format = SsTableFormat::default();
let table_store = Arc::new(TableStore::new(
object_stores,
format.clone(),
Path::from("/root"),
None,
TableStoreKind::GC,
));
let manifest_store = Arc::new(ManifestStore::new(&Path::from("/root"), main_store.clone()));
let mut stored_manifest = StoredManifest::create_new_db(
manifest_store.clone(),
ManifestCore::new(),
Arc::new(DefaultSystemClock::new()),
)
.await
.unwrap();
let compactions_store = Arc::new(CompactionsStore::new(
&Path::from("/root"),
main_store.clone(),
));
StoredCompactions::create(
compactions_store.clone(),
stored_manifest.manifest().compactor_epoch,
)
.await
.unwrap();
let l0_id = SsTableId::Compacted(ulid::Ulid::from_parts(9_000, 0));
let l0_handle = table_store
.write_sst(&l0_id, &build_test_sst(&format, 1).await, false)
.await
.unwrap();
let mut dirty_manifest = stored_manifest.prepare_dirty().unwrap();
Arc::make_mut(&mut dirty_manifest.value.core.tree)
.l0
.push_back(SsTableView::identity(l0_handle));
stored_manifest.update(dirty_manifest).await.unwrap();
let compaction_output_id = SsTableId::Compacted(ulid::Ulid::from_parts(6_000, 0));
table_store
.write_sst(
&compaction_output_id,
&build_test_sst(&format, 1).await,
false,
)
.await
.unwrap();
let opts = GarbageCollectorDirectoryOptions {
interval: None,
min_age: Duration::from_secs(2),
dry_run: false,
};
let recorder = slatedb_common::metrics::MetricsRecorderHelper::noop();
let stats = Arc::new(GcStats::new(&recorder));
let task = CompactedGcTask::new(
manifest_store.clone(),
compactions_store.clone(),
table_store.clone(),
stats,
opts,
None,
);
let utc_now = DateTime::<Utc>::from_timestamp_millis(10_000).unwrap();
task.collect(utc_now).await.unwrap();
let remaining: Vec<_> = table_store
.list_compacted_ssts(..)
.await
.unwrap()
.into_iter()
.map(|m| m.id)
.collect();
assert!(
remaining.contains(&compaction_output_id),
"expected GC to retain compacted SST output from a running compaction when the watermark is missing"
);
}
fn ulid_at(seq: u64) -> ulid::Ulid {
ulid::Ulid::from_parts(seq, 0)
}
fn view_at(seq: u64) -> SsTableView {
let id = ulid_at(seq);
SsTableView::identity(SsTableHandle::new(
SsTableId::Compacted(id),
SST_FORMAT_VERSION_LATEST,
SsTableInfo::default(),
))
}
fn manifest_with(unsegmented: LsmTreeState, segments: Vec<Segment>) -> Manifest {
let mut core = ManifestCore::new();
core.tree = Arc::new(unsegmented);
core.segments = segments;
if !core.segments.is_empty() {
core.segment_extractor_name = Some("test".into());
}
Manifest::initial(core)
}
#[test]
fn test_collect_active_ssts_walks_segment_trees() {
let unsegmented_l0 = view_at(1);
let segment_l0 = view_at(2);
let segment_sr = view_at(3);
let manifest = manifest_with(
LsmTreeState {
last_compacted_l0_sst_view_id: None,
last_compacted_l0_sst_id: None,
l0: VecDeque::from(vec![unsegmented_l0.clone()]),
compacted: vec![],
},
vec![Segment {
prefix: Bytes::from_static(b"hour=12/"),
tree: Arc::new(LsmTreeState {
last_compacted_l0_sst_view_id: None,
last_compacted_l0_sst_id: None,
l0: VecDeque::from(vec![segment_l0.clone()]),
compacted: vec![SortedRun {
id: 0,
sst_views: vec![segment_sr.clone()],
}],
}),
}],
);
let mut by_id = BTreeMap::new();
by_id.insert(1, manifest);
let active = collect_active_ssts(by_id.values());
assert!(active.contains(&unsegmented_l0.sst.id));
assert!(active.contains(&segment_l0.sst.id));
assert!(active.contains(&segment_sr.sst.id));
}
#[test]
fn test_newest_l0_dt_considers_segment_l0() {
let manifest = manifest_with(
LsmTreeState {
last_compacted_l0_sst_view_id: None,
last_compacted_l0_sst_id: None,
l0: VecDeque::from(vec![view_at(1_000)]),
compacted: vec![],
},
vec![Segment {
prefix: Bytes::from_static(b"hour=12/"),
tree: Arc::new(LsmTreeState {
last_compacted_l0_sst_view_id: None,
last_compacted_l0_sst_id: None,
l0: VecDeque::from(vec![view_at(5_000)]),
compacted: vec![],
}),
}],
);
assert_eq!(
newest_l0_dt(&manifest),
DateTime::<Utc>::from(ulid_at(5_000).datetime())
);
}
#[test]
fn test_newest_l0_dt_falls_back_to_segment_watermark() {
let manifest = manifest_with(
LsmTreeState::default(),
vec![Segment {
prefix: Bytes::from_static(b"hour=12/"),
tree: Arc::new(LsmTreeState {
last_compacted_l0_sst_view_id: Some(ulid_at(7_000)),
last_compacted_l0_sst_id: None,
l0: VecDeque::new(),
compacted: vec![],
}),
}],
);
assert_eq!(
newest_l0_dt(&manifest),
DateTime::<Utc>::from(ulid_at(7_000).datetime())
);
}
#[test]
fn test_newest_l0_dt_max_across_unsegmented_and_segments() {
let manifest = manifest_with(
LsmTreeState {
last_compacted_l0_sst_view_id: Some(ulid_at(2_000)),
last_compacted_l0_sst_id: None,
l0: VecDeque::new(),
compacted: vec![],
},
vec![Segment {
prefix: Bytes::from_static(b"hour=12/"),
tree: Arc::new(LsmTreeState {
last_compacted_l0_sst_view_id: None,
last_compacted_l0_sst_id: None,
l0: VecDeque::from(vec![view_at(9_000)]),
compacted: vec![],
}),
}],
);
assert_eq!(
newest_l0_dt(&manifest),
DateTime::<Utc>::from(ulid_at(9_000).datetime())
);
}
#[test]
fn test_newest_l0_dt_unix_epoch_when_no_trees_contribute() {
let manifest = manifest_with(LsmTreeState::default(), vec![]);
assert_eq!(newest_l0_dt(&manifest), DateTime::<Utc>::UNIX_EPOCH);
}
}