1use super::refs::TagContents;
37use crate::dataset::TRANSACTIONS_DIR;
38use crate::{Dataset, utils::temporal::utc_now};
39use chrono::{DateTime, TimeDelta, Utc};
40use dashmap::DashSet;
41use futures::future::try_join_all;
42use futures::stream::BoxStream;
43use futures::{StreamExt, TryStreamExt, stream};
44use humantime::parse_duration;
45use lance_core::{
46 Error, Result,
47 utils::tracing::{
48 AUDIT_MODE_DELETE, AUDIT_MODE_DELETE_UNVERIFIED, AUDIT_TYPE_DATA, AUDIT_TYPE_DELETION,
49 AUDIT_TYPE_INDEX, AUDIT_TYPE_MANIFEST, DATASET_CLEANING_EVENT, TRACE_DATASET_EVENTS,
50 TRACE_FILE_AUDIT,
51 },
52};
53use lance_table::{
54 format::{IndexMetadata, Manifest},
55 io::{
56 commit::ManifestLocation,
57 deletion::deletion_file_path,
58 manifest::{read_manifest, read_manifest_indexes},
59 },
60};
61use object_store::ObjectMeta;
62use object_store::path::Path;
63use std::fmt::Debug;
64use std::{
65 collections::{HashMap, HashSet},
66 future,
67 sync::{Mutex, MutexGuard},
68 time::Duration,
69};
70use tokio::time::{MissedTickBehavior, interval};
71use tokio_stream::wrappers::IntervalStream;
72use tracing::{Span, debug, info, instrument};
73
74#[derive(Clone, Debug, Default)]
75struct ReferencedFiles {
76 data_paths: HashSet<Path>,
77 delete_paths: HashSet<Path>,
78 tx_paths: HashSet<Path>,
79 index_uuids: HashSet<String>,
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
83pub struct RemovalStats {
84 pub bytes_removed: u64,
85 pub old_versions: u64,
86 pub data_files_removed: u64,
87 pub transaction_files_removed: u64,
88 pub index_files_removed: u64,
89 pub deletion_files_removed: u64,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct CleanupExplanation {
99 pub read_version: u64,
101 pub stats: RemovalStats,
103 pub candidate_files: Vec<CleanupCandidateFile>,
105 pub candidate_files_truncated: bool,
107 pub candidate_file_limit: usize,
109 pub referenced_branches: Vec<CleanupReferencedBranch>,
111 pub warnings: Vec<String>,
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct CleanupCandidateFile {
118 pub path: String,
120 pub kind: CleanupFileKind,
122 pub unverified: bool,
125 pub size_bytes: u64,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct CleanupReferencedBranch {
132 pub name: String,
134 pub referenced_version: u64,
136 pub cleanup_candidate: bool,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum CleanupFileKind {
142 Manifest,
143 Data,
144 Transaction,
145 Index,
146 Deletion,
147 TemporaryManifest,
152}
153
154impl CleanupCandidateFile {
155 fn from_cleanup_file(file: &CleanupFile) -> Self {
156 Self {
157 path: file.path.to_string(),
158 kind: file.kind,
159 unverified: file.unverified,
160 size_bytes: file.size_bytes,
161 }
162 }
163}
164
165fn cleanup_file(
166 path: Path,
167 kind: CleanupFileKind,
168 unverified: bool,
169 size_bytes: u64,
170) -> Option<CleanupFile> {
171 Some(CleanupFile {
172 path,
173 kind,
174 unverified,
175 size_bytes,
176 })
177}
178
179#[derive(Clone, Debug)]
180struct CleanupFile {
181 path: Path,
182 kind: CleanupFileKind,
183 unverified: bool,
188 size_bytes: u64,
189}
190
191impl RemovalStats {
192 fn record_file(&mut self, file: &CleanupFile) {
193 self.bytes_removed += file.size_bytes;
194 match file.kind {
195 CleanupFileKind::Manifest => self.old_versions += 1,
196 CleanupFileKind::Data => self.data_files_removed += 1,
197 CleanupFileKind::Transaction => self.transaction_files_removed += 1,
198 CleanupFileKind::Index => self.index_files_removed += 1,
199 CleanupFileKind::Deletion => self.deletion_files_removed += 1,
200 CleanupFileKind::TemporaryManifest => {}
201 }
202 }
203
204 fn merge(&mut self, other: &Self) {
205 self.bytes_removed += other.bytes_removed;
206 self.old_versions += other.old_versions;
207 self.data_files_removed += other.data_files_removed;
208 self.transaction_files_removed += other.transaction_files_removed;
209 self.index_files_removed += other.index_files_removed;
210 self.deletion_files_removed += other.deletion_files_removed;
211 }
212}
213
214#[derive(Debug, Default)]
215struct CleanupRunResult {
216 stats: RemovalStats,
217 removed_manifests: HashSet<Path>,
218 candidate_files: Vec<CleanupCandidateFile>,
219 candidate_files_truncated: bool,
220 referenced_branches: Vec<CleanupReferencedBranch>,
221}
222
223impl CleanupRunResult {
224 fn record_file(
225 &mut self,
226 file: &CleanupFile,
227 candidate_file_limit: Option<usize>,
228 track_removed_manifests: bool,
229 ) {
230 self.stats.record_file(file);
231 if track_removed_manifests && matches!(file.kind, CleanupFileKind::Manifest) {
232 self.removed_manifests.insert(file.path.clone());
233 }
234 if let Some(limit) = candidate_file_limit {
235 if self.candidate_files.len() < limit {
236 self.candidate_files
237 .push(CleanupCandidateFile::from_cleanup_file(file));
238 } else {
239 self.candidate_files_truncated = true;
240 }
241 }
242 }
243
244 fn merge(&mut self, other: Self, candidate_file_limit: Option<usize>) {
245 self.stats.merge(&other.stats);
246 self.removed_manifests.extend(other.removed_manifests);
247 self.referenced_branches.extend(other.referenced_branches);
248 if let Some(limit) = candidate_file_limit {
249 for file in other.candidate_files {
250 if self.candidate_files.len() < limit {
251 self.candidate_files.push(file);
252 } else {
253 self.candidate_files_truncated = true;
254 }
255 }
256 self.candidate_files_truncated |= other.candidate_files_truncated;
257 }
258 }
259}
260
261#[derive(Clone, Copy, Debug)]
262enum CleanupAction {
263 Execute,
264 Explain { max_candidate_files: usize },
265}
266
267impl CleanupAction {
268 fn deletes_files(self) -> bool {
269 matches!(self, Self::Execute)
270 }
271
272 fn candidate_file_limit(self) -> Option<usize> {
273 match self {
274 Self::Execute => None,
275 Self::Explain {
276 max_candidate_files,
277 } => Some(max_candidate_files),
278 }
279 }
280}
281
282fn remove_prefix(path: &Path, prefix: &Path) -> Path {
283 let relative_parts = path.prefix_match(prefix);
284 if relative_parts.is_none() {
285 return path.clone();
286 }
287 Path::from_iter(relative_parts.unwrap())
288}
289
290#[derive(Clone, Debug)]
291struct CleanupTask<'a> {
292 dataset: &'a Dataset,
293 policy: CleanupPolicy,
294 action: CleanupAction,
295 read_version: u64,
296 ignored_manifests: HashSet<Path>,
297 track_removed_manifests: bool,
298 include_referenced_branches: bool,
299}
300
301#[derive(Clone, Debug, Default)]
303struct CleanupInspection {
304 old_manifests: HashMap<Path, u64>,
305 referenced_files: ReferencedFiles,
307 verified_files: ReferencedFiles,
311 tagged_old_versions: HashSet<u64>,
313 earliest_retained_manifest_time: Option<DateTime<Utc>>,
315}
316
317const UNVERIFIED_THRESHOLD_DAYS: i64 = 7;
320const S3_DELETE_STREAM_BATCH_SIZE: u64 = 1_000;
321const AZURE_DELETE_STREAM_BATCH_SIZE: u64 = 256;
322const DEFAULT_EXPLANATION_MAX_CANDIDATE_FILES: usize = 1_000;
323
324pub struct CleanupOperation<'a> {
330 dataset: &'a Dataset,
331 policy: CleanupPolicy,
332 max_candidate_files: usize,
333}
334
335impl<'a> CleanupOperation<'a> {
336 pub(crate) fn new(dataset: &'a Dataset, policy: CleanupPolicy) -> Self {
337 Self {
338 dataset,
339 policy,
340 max_candidate_files: DEFAULT_EXPLANATION_MAX_CANDIDATE_FILES,
341 }
342 }
343
344 pub fn with_max_candidate_files(mut self, max_candidate_files: usize) -> Self {
349 self.max_candidate_files = max_candidate_files;
350 self
351 }
352
353 pub async fn explain(&self) -> Result<CleanupExplanation> {
355 let cleanup = CleanupTask::new(
356 self.dataset,
357 self.policy.clone(),
358 CleanupAction::Explain {
359 max_candidate_files: self.max_candidate_files,
360 },
361 );
362 let read_version = cleanup.read_version;
363 let result = cleanup.run().await?;
364 let warnings = if result.candidate_files_truncated {
365 vec![format!(
366 "candidate_files truncated to {} entries",
367 self.max_candidate_files
368 )]
369 } else {
370 Vec::new()
371 };
372 Ok(CleanupExplanation {
373 read_version,
374 stats: result.stats,
375 candidate_files: result.candidate_files,
376 candidate_files_truncated: result.candidate_files_truncated,
377 candidate_file_limit: self.max_candidate_files,
378 referenced_branches: result.referenced_branches,
379 warnings,
380 })
381 }
382
383 pub async fn execute(&self) -> Result<RemovalStats> {
385 info!(target: TRACE_DATASET_EVENTS, event=DATASET_CLEANING_EVENT, uri=&self.dataset.uri);
386 let cleanup = CleanupTask::new(self.dataset, self.policy.clone(), CleanupAction::Execute);
387 Ok(cleanup.run().await?.stats)
388 }
389}
390
391impl<'a> CleanupTask<'a> {
392 fn new(dataset: &'a Dataset, policy: CleanupPolicy, action: CleanupAction) -> Self {
393 let track_removed_manifests = policy.clean_referenced_branches;
394 let include_referenced_branches = action.candidate_file_limit().is_some();
395 Self::new_with_ignored_manifests(
396 dataset,
397 policy,
398 action,
399 HashSet::new(),
400 track_removed_manifests,
401 include_referenced_branches,
402 )
403 }
404
405 fn new_with_ignored_manifests(
406 dataset: &'a Dataset,
407 policy: CleanupPolicy,
408 action: CleanupAction,
409 ignored_manifests: HashSet<Path>,
410 track_removed_manifests: bool,
411 include_referenced_branches: bool,
412 ) -> Self {
413 Self {
414 dataset,
415 policy,
416 action,
417 read_version: dataset.version().version,
418 ignored_manifests,
419 track_removed_manifests,
420 include_referenced_branches,
421 }
422 }
423
424 async fn run(self) -> Result<CleanupRunResult> {
425 let mut final_result = CleanupRunResult::default();
426 let candidate_file_limit = self.action.candidate_file_limit();
427 let referenced_branches: Vec<(String, u64)> = self.find_referenced_branches().await?;
432 if self.include_referenced_branches {
433 final_result.referenced_branches = referenced_branches
434 .iter()
435 .map(|(name, referenced_version)| CleanupReferencedBranch {
436 name: name.clone(),
437 referenced_version: *referenced_version,
438 cleanup_candidate: self.policy.clean_referenced_branches,
439 })
440 .collect();
441 }
442 if self.policy.clean_referenced_branches {
443 final_result.merge(
444 self.clean_referenced_branches(&referenced_branches).await?,
445 candidate_file_limit,
446 );
447 }
448
449 let tags = self.dataset.tags().list().await?;
456 let current_branch = &self.dataset.manifest.branch;
457
458 let tagged_versions: HashSet<u64> = tags
461 .values()
462 .filter(|tag| match (tag.branch.as_ref(), current_branch.as_ref()) {
463 (Some(branch_of_tag), Some(current_branch)) => branch_of_tag == current_branch,
464 (None, None) => true,
465 _ => false,
466 })
467 .map(|tag_content| tag_content.version)
468 .collect();
469
470 let mut inspection = self.process_manifests(&tagged_versions).await?;
471
472 if self.policy.error_if_tagged_old_versions && !inspection.tagged_old_versions.is_empty() {
473 return Err(tagged_old_versions_cleanup_error(
474 &tags,
475 &inspection.tagged_old_versions,
476 ));
477 }
478
479 if !referenced_branches.is_empty() {
480 let ignored_manifests: HashSet<_> = final_result
481 .removed_manifests
482 .union(&self.ignored_manifests)
483 .cloned()
484 .collect();
485 inspection = self
486 .retain_branch_lineage_files(inspection, &referenced_branches, &ignored_manifests)
487 .await?
488 };
489
490 final_result.merge(
491 self.delete_unreferenced_files(inspection).await?,
492 candidate_file_limit,
493 );
494 Ok(final_result)
495 }
496
497 #[instrument(level = "debug", skip_all)]
498 async fn process_manifests(
499 &'a self,
500 tagged_versions: &HashSet<u64>,
501 ) -> Result<CleanupInspection> {
502 let inspection = Mutex::new(CleanupInspection::default());
503 self.dataset
504 .commit_handler
505 .list_manifest_locations(&self.dataset.base, &self.dataset.object_store, false)
506 .try_filter(|location| future::ready(!self.ignored_manifests.contains(&location.path)))
507 .try_for_each_concurrent(self.dataset.object_store.io_parallelism(), |location| {
508 self.process_manifest_file(location, &inspection, tagged_versions)
509 })
510 .await?;
511 Ok(inspection.into_inner().unwrap())
512 }
513
514 async fn process_manifest_file(
515 &self,
516 location: ManifestLocation,
517 inspection: &Mutex<CleanupInspection>,
518 tagged_versions: &HashSet<u64>,
519 ) -> Result<()> {
520 let manifest =
527 read_manifest(&self.dataset.object_store, &location.path, location.size).await?;
528 let is_latest = self.read_version <= manifest.version;
532 let is_tagged = tagged_versions.contains(&manifest.version);
533 let in_working_set = is_latest || !self.policy.should_clean(&manifest) || is_tagged;
534 let indexes =
535 read_manifest_indexes(&self.dataset.object_store, &location, &manifest).await?;
536
537 let mut inspection = inspection.lock().unwrap();
538
539 if is_tagged && !is_latest && self.policy.should_clean(&manifest) {
542 inspection.tagged_old_versions.insert(manifest.version);
543 }
544
545 self.process_manifest(&manifest, &indexes, in_working_set, &mut inspection)?;
546 if !in_working_set {
547 inspection
548 .old_manifests
549 .insert(location.path.clone(), manifest.version);
550 } else {
551 let commit_ts = manifest.timestamp();
552 if let Some(ts) = inspection.earliest_retained_manifest_time {
553 if commit_ts < ts {
554 inspection.earliest_retained_manifest_time = Some(commit_ts);
555 }
556 } else {
557 inspection.earliest_retained_manifest_time = Some(commit_ts);
558 }
559 }
560 Ok(())
561 }
562
563 fn process_manifest(
564 &self,
565 manifest: &Manifest,
566 indexes: &Vec<IndexMetadata>,
567 in_working_set: bool,
568 inspection: &mut MutexGuard<CleanupInspection>,
569 ) -> Result<()> {
570 let referenced_files = if in_working_set {
573 &mut inspection.referenced_files
574 } else {
575 &mut inspection.verified_files
576 };
577
578 for fragment in manifest.fragments.iter() {
579 for file in fragment.files.iter() {
580 let full_data_path = self.dataset.data_dir().clone().join(file.path.as_str());
581 let relative_data_path = remove_prefix(&full_data_path, &self.dataset.base);
582 referenced_files.data_paths.insert(relative_data_path);
583 }
584 let delpath = fragment
585 .deletion_file
586 .as_ref()
587 .map(|delfile| deletion_file_path(&self.dataset.base, fragment.id, delfile));
588 if let Some(delpath) = delpath {
589 let relative_path = remove_prefix(&delpath, &self.dataset.base);
590 referenced_files.delete_paths.insert(relative_path);
591 }
592 }
593 if let Some(relative_tx_path) = &manifest.transaction_file {
594 referenced_files
595 .tx_paths
596 .insert(Path::parse(TRANSACTIONS_DIR)?.join(relative_tx_path.as_str()));
597 }
598
599 for index in indexes {
600 let uuid_str = index.uuid.to_string();
601 referenced_files.index_uuids.insert(uuid_str);
602 }
603 Ok(())
604 }
605
606 #[instrument(
607 level = "debug",
608 skip_all,
609 fields(
610 old_versions = inspection.old_manifests.len(),
611 bytes_removed = tracing::field::Empty,
612 data_files_removed = tracing::field::Empty,
613 transaction_files_removed = tracing::field::Empty,
614 index_files_removed = tracing::field::Empty,
615 deletion_files_removed = tracing::field::Empty
616 )
617 )]
618 async fn delete_unreferenced_files(
619 &self,
620 inspection: CleanupInspection,
621 ) -> Result<CleanupRunResult> {
622 let cleanup_result = Mutex::new(CleanupRunResult::default());
623 let deletes_files = self.action.deletes_files();
624 let candidate_file_limit = self.action.candidate_file_limit();
625 let verification_threshold = utc_now()
626 - TimeDelta::try_days(UNVERIFIED_THRESHOLD_DAYS).expect("TimeDelta::try_days");
627
628 let is_not_found_err = |e: &Error| matches!(e, Error::NotFound { .. });
629 let build_listing_stream = |dir: Path| {
631 let inspection_ref = &inspection;
632 self.dataset
633 .object_store
634 .read_dir_all(&dir, inspection.earliest_retained_manifest_time)
635 .map_ok(|obj| stream::once(future::ready(Ok(obj))).boxed())
636 .or_else(|e| {
637 if is_not_found_err(&e) {
639 future::ready(Ok(stream::empty::<Result<ObjectMeta>>().boxed()))
640 } else {
641 future::ready(Err(e))
642 }
643 })
644 .try_flatten()
645 .try_filter_map(move |obj_meta| {
646 let maybe_in_progress = !self.policy.delete_unverified
649 && obj_meta.last_modified >= verification_threshold;
650 let file_to_remove = self.cleanup_file_if_not_referenced(
651 obj_meta,
652 maybe_in_progress,
653 inspection_ref,
654 );
655 future::ready(file_to_remove)
656 })
657 .boxed()
658 };
659
660 let streams = vec![
662 build_listing_stream(self.dataset.versions_dir()),
663 build_listing_stream(self.dataset.transactions_dir()),
664 build_listing_stream(self.dataset.data_dir()),
665 build_listing_stream(self.dataset.indices_dir()),
666 build_listing_stream(self.dataset.deletions_dir()),
667 ];
668 let unreferenced_files = stream::iter(streams).flatten().boxed();
669
670 let old_manifests = inspection.old_manifests.clone();
671 let manifest_files = stream::iter(old_manifests)
672 .map(|(path, _version)| async move {
673 let size_bytes = self.dataset.object_store.size(&path).await?;
674 Ok::<CleanupFile, Error>(CleanupFile {
675 path,
676 kind: CleanupFileKind::Manifest,
677 unverified: false,
678 size_bytes,
679 })
680 })
681 .buffer_unordered(self.dataset.object_store.io_parallelism())
682 .boxed();
683
684 let all_files = stream::iter(vec![unreferenced_files, manifest_files]).flatten();
685 let all_paths_to_remove = all_files.map(|file| {
686 let file = file?;
687 if deletes_files {
688 let mode = if file.unverified {
689 AUDIT_MODE_DELETE_UNVERIFIED
690 } else {
691 AUDIT_MODE_DELETE
692 };
693 let path_str = file.path.as_ref();
694 match file.kind {
695 CleanupFileKind::Manifest => {
696 info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = path_str);
697 }
698 CleanupFileKind::Data => {
699 info!(target: TRACE_FILE_AUDIT, mode=mode, r#type=AUDIT_TYPE_DATA, path = path_str);
700 }
701 CleanupFileKind::Deletion => {
702 info!(target: TRACE_FILE_AUDIT, mode=mode, r#type=AUDIT_TYPE_DELETION, path = path_str);
703 }
704 CleanupFileKind::Index => {
705 info!(target: TRACE_FILE_AUDIT, mode=mode, r#type=AUDIT_TYPE_INDEX, path = path_str);
706 }
707 CleanupFileKind::Transaction | CleanupFileKind::TemporaryManifest => {}
708 }
709 }
710 cleanup_result
711 .lock()
712 .unwrap()
713 .record_file(&file, candidate_file_limit, self.track_removed_manifests);
714 Ok(file.path)
715 });
716
717 if deletes_files {
718 let paths_to_delete: BoxStream<Result<Path>> =
719 if let Some(rate) = self.policy.delete_rate_limit {
720 let duration =
721 calculate_duration(self.dataset.object_store.scheme().to_string(), rate);
722 let mut ticker = interval(duration);
723 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
724 IntervalStream::new(ticker)
725 .zip(all_paths_to_remove)
726 .map(|(_, path)| path)
727 .boxed()
728 } else {
729 all_paths_to_remove.boxed()
730 };
731
732 self.dataset
733 .object_store
734 .remove_stream(paths_to_delete)
735 .try_for_each(|_| future::ready(Ok(())))
736 .await?;
737 } else {
738 all_paths_to_remove
740 .try_for_each(|_| future::ready(Ok(())))
741 .await?;
742 }
743
744 let cleanup_result = cleanup_result.into_inner().unwrap();
745
746 let span = Span::current();
747 span.record("bytes_removed", cleanup_result.stats.bytes_removed);
748 span.record(
749 "data_files_removed",
750 cleanup_result.stats.data_files_removed,
751 );
752 span.record(
753 "transaction_files_removed",
754 cleanup_result.stats.transaction_files_removed,
755 );
756 span.record(
757 "index_files_removed",
758 cleanup_result.stats.index_files_removed,
759 );
760 span.record(
761 "deletion_files_removed",
762 cleanup_result.stats.deletion_files_removed,
763 );
764
765 Ok(cleanup_result)
766 }
767
768 fn cleanup_file_if_not_referenced(
769 &self,
770 obj_meta: ObjectMeta,
771 maybe_in_progress: bool,
772 inspection: &CleanupInspection,
773 ) -> Result<Option<CleanupFile>> {
774 let path = obj_meta.location;
775 let relative_path = remove_prefix(&path, &self.dataset.base);
776 let size_bytes = obj_meta.size;
777 if relative_path.as_ref().starts_with("_versions/.tmp") {
778 if maybe_in_progress {
783 return Ok(None);
784 } else {
785 return Ok(cleanup_file(
786 path,
787 CleanupFileKind::TemporaryManifest,
788 true,
789 size_bytes,
790 ));
791 }
792 }
793 if relative_path.as_ref().starts_with("_indices") {
794 if let Some(uuid) = relative_path.parts().nth(1) {
797 if inspection
798 .referenced_files
799 .index_uuids
800 .contains(uuid.as_ref())
801 {
802 return Ok(None);
803 } else if !maybe_in_progress {
804 return Ok(cleanup_file(path, CleanupFileKind::Index, true, size_bytes));
805 } else if inspection
806 .verified_files
807 .index_uuids
808 .contains(uuid.as_ref())
809 {
810 return Ok(cleanup_file(
811 path,
812 CleanupFileKind::Index,
813 false,
814 size_bytes,
815 ));
816 }
817 } else {
818 return Ok(None);
819 }
820 }
821 match path.extension() {
822 Some("lance") => {
823 if relative_path.as_ref().starts_with("data") {
824 if inspection
825 .referenced_files
826 .data_paths
827 .contains(&relative_path)
828 {
829 Ok(None)
830 } else if !maybe_in_progress {
831 Ok(cleanup_file(path, CleanupFileKind::Data, true, size_bytes))
832 } else if inspection
833 .verified_files
834 .data_paths
835 .contains(&relative_path)
836 {
837 Ok(cleanup_file(path, CleanupFileKind::Data, false, size_bytes))
838 } else {
839 Ok(None)
840 }
841 } else {
842 Ok(None)
844 }
845 }
846 Some("blob") => {
847 if !relative_path.as_ref().starts_with("data") {
853 debug!(
854 path = relative_path.as_ref(),
855 "Will not garbage collect blob file because it does not follow convention"
856 );
857 return Ok(None);
858 }
859
860 let mut parts = relative_path.parts();
861 let data_dir = parts.next();
862 let data_file_key = parts.next();
863 let blob_file = parts.next();
864 if !matches!(data_dir, Some(dir) if dir.as_ref() == "data")
866 || data_file_key.is_none()
867 || blob_file.is_none()
868 {
869 debug!(
870 path = relative_path.as_ref(),
871 "Will not garbage collect blob file because it does not follow convention"
872 );
873 return Ok(None);
874 }
875 if parts.next().is_some() {
876 debug!(
877 path = relative_path.as_ref(),
878 "Will not garbage collect blob file because it does not follow convention"
879 );
880 return Ok(None);
881 }
882
883 let data_file_key = data_file_key.expect("checked is_some");
884 let Ok(parent_data_path) =
885 Path::parse(format!("data/{}.lance", data_file_key.as_ref()))
886 else {
887 debug!(
888 path = relative_path.as_ref(),
889 derived_parent = format!("data/{}.lance", data_file_key.as_ref()),
890 "Will not garbage collect blob file because derived parent data file path is invalid"
891 );
892 return Ok(None);
893 };
894
895 if inspection
896 .referenced_files
897 .data_paths
898 .contains(&parent_data_path)
899 {
900 Ok(None)
901 } else if !maybe_in_progress {
902 Ok(cleanup_file(path, CleanupFileKind::Data, true, size_bytes))
903 } else if inspection
904 .verified_files
905 .data_paths
906 .contains(&parent_data_path)
907 {
908 Ok(cleanup_file(path, CleanupFileKind::Data, false, size_bytes))
909 } else {
910 Ok(None)
911 }
912 }
913 Some("manifest") => {
914 Ok(None)
916 }
917 Some("arrow") | Some("bin") => {
918 if relative_path.as_ref().starts_with("_deletions") {
919 if inspection
920 .referenced_files
921 .delete_paths
922 .contains(&relative_path)
923 {
924 Ok(None)
925 } else if !maybe_in_progress {
926 Ok(cleanup_file(
927 path,
928 CleanupFileKind::Deletion,
929 true,
930 size_bytes,
931 ))
932 } else if inspection
933 .verified_files
934 .delete_paths
935 .contains(&relative_path)
936 {
937 Ok(cleanup_file(
938 path,
939 CleanupFileKind::Deletion,
940 false,
941 size_bytes,
942 ))
943 } else {
944 Ok(None)
945 }
946 } else {
947 Ok(None)
948 }
949 }
950 Some("txn") => {
951 if relative_path.as_ref().starts_with(TRANSACTIONS_DIR) {
952 if inspection
953 .referenced_files
954 .tx_paths
955 .contains(&relative_path)
956 {
957 Ok(None)
958 } else if !maybe_in_progress
959 || inspection.verified_files.tx_paths.contains(&relative_path)
960 {
961 let unverified =
962 !inspection.verified_files.tx_paths.contains(&relative_path);
963 Ok(cleanup_file(
964 path,
965 CleanupFileKind::Transaction,
966 unverified,
967 size_bytes,
968 ))
969 } else {
970 Ok(None)
971 }
972 } else {
973 Ok(None)
974 }
975 }
976 _ => Ok(None),
977 }
978 }
979
980 async fn find_referenced_branches(&self) -> Result<Vec<(String, u64)>> {
981 let current_branch_id = self.dataset.branch_identifier().await?;
982 let all_branches = self.dataset.branches().list().await?;
983 let children = current_branch_id.collect_referenced_versions(&all_branches);
984
985 let referenced_branches: DashSet<String> = DashSet::new();
988 let tasks: Vec<_> = children
989 .iter()
990 .map(|(branch_name, referenced_version)| {
991 let dataset = &self.dataset;
992 let policy = &self.policy;
993 let referenced_branches = &referenced_branches;
994
995 async move {
996 let manifest_location = dataset
997 .commit_handler
998 .resolve_version_location(
999 &dataset.base,
1000 *referenced_version,
1001 &dataset.object_store.inner,
1002 )
1003 .await?;
1004
1005 let manifest = read_manifest(
1006 &dataset.object_store,
1007 &manifest_location.path,
1008 manifest_location.size,
1009 )
1010 .await;
1011
1012 if let Ok(manifest) = manifest
1013 && policy.should_clean(&manifest)
1014 {
1015 referenced_branches.insert(branch_name.clone());
1016 }
1017 Ok::<(), Error>(())
1018 }
1019 })
1020 .collect();
1021
1022 try_join_all(tasks).await?;
1023
1024 let referenced_branches = children
1027 .iter()
1028 .filter(|(branch_name, _)| referenced_branches.contains(branch_name))
1029 .cloned()
1030 .collect();
1031 Ok(referenced_branches)
1032 }
1033
1034 async fn clean_referenced_branches(
1035 &self,
1036 referenced_branches: &[(String, u64)],
1037 ) -> Result<CleanupRunResult> {
1038 let final_result = Mutex::new(CleanupRunResult::default());
1039
1040 let mut branches_chains = HashMap::new();
1044 for (branch, id) in referenced_branches {
1045 branches_chains
1046 .entry(*id)
1047 .or_insert_with(Vec::new)
1048 .push(branch.clone());
1049 }
1050 let action = self.action;
1051 let candidate_file_limit = self.action.candidate_file_limit();
1052 let tasks: Vec<_> = branches_chains
1053 .values()
1054 .map(|branch_chain| {
1055 let final_result = &final_result;
1056 async move {
1057 for branch in branch_chain {
1058 let branch_dataset = self
1059 .dataset
1060 .checkout_version((branch.as_str(), None))
1061 .await?;
1062 let ignored_manifests =
1063 final_result.lock().unwrap().removed_manifests.clone();
1064 if let Some(result) = cleanup_cascade_branch_run(
1065 &branch_dataset,
1066 branch_dataset.manifest.as_ref(),
1067 action,
1068 ignored_manifests,
1069 )
1070 .await?
1071 {
1072 final_result
1073 .lock()
1074 .unwrap()
1075 .merge(result, candidate_file_limit);
1076 }
1077 }
1078 Ok::<(), Error>(())
1079 }
1080 })
1081 .collect();
1082 try_join_all(tasks).await?;
1083 Ok(final_result.into_inner().unwrap())
1084 }
1085
1086 async fn retain_branch_lineage_files(
1089 &self,
1090 inspection: CleanupInspection,
1091 referenced_branches: &[(String, u64)],
1092 removed_branch_manifests: &HashSet<Path>,
1093 ) -> Result<CleanupInspection> {
1094 let inspection = Mutex::new(inspection);
1095 for (branch, root_version_number) in referenced_branches {
1096 let branch_location = self.dataset.branch_location().find_branch(Some(branch))?;
1100 self.dataset
1101 .commit_handler
1102 .list_manifest_locations(&branch_location.path, &self.dataset.object_store, false)
1103 .try_filter(|location| {
1104 future::ready(!removed_branch_manifests.contains(&location.path))
1105 })
1106 .try_for_each_concurrent(self.dataset.object_store.io_parallelism(), |location| {
1107 self.process_branch_referenced_manifests(
1108 location,
1109 *root_version_number,
1110 &inspection,
1111 )
1112 })
1113 .await?;
1114 }
1115 Ok(inspection.into_inner().unwrap())
1116 }
1117
1118 async fn process_branch_referenced_manifests(
1119 &self,
1120 location: ManifestLocation,
1121 referenced_version: u64,
1122 inspection: &Mutex<CleanupInspection>,
1123 ) -> Result<()> {
1124 let manifest =
1125 read_manifest(&self.dataset.object_store, &location.path, location.size).await?;
1126 let indexes =
1127 read_manifest_indexes(&self.dataset.object_store, &location, &manifest).await?;
1128 let mut inspection = inspection.lock().unwrap();
1129 let mut is_referenced = false;
1130
1131 for fragment in manifest.fragments.iter() {
1132 for file in fragment.files.iter() {
1133 if let Some(base_id) = file.base_id {
1134 let base_path = manifest.base_paths.get(&base_id);
1135 if let Some(base_path) = base_path
1136 && base_path.path == self.dataset.uri
1137 {
1138 let full_data_path =
1139 self.dataset.data_dir().clone().join(file.path.as_str());
1140 let relative_data_path = remove_prefix(&full_data_path, &self.dataset.base);
1141 inspection
1142 .verified_files
1143 .data_paths
1144 .remove(&relative_data_path);
1145 inspection
1146 .referenced_files
1147 .data_paths
1148 .insert(relative_data_path);
1149 is_referenced = true;
1150 }
1151 }
1152 }
1153 if let Some(del_file) = fragment.deletion_file.as_ref()
1154 && let Some(base_id) = del_file.base_id
1155 {
1156 let base_path = manifest.base_paths.get(&base_id);
1157 if let Some(base_path) = base_path {
1158 let deletion_path = fragment.deletion_file.as_ref().map(|deletion_file| {
1159 deletion_file_path(&self.dataset.base, fragment.id, deletion_file)
1160 });
1161 if base_path.path == self.dataset.uri {
1162 if let Some(deletion_path) = deletion_path {
1163 let relative_del_path =
1164 remove_prefix(&deletion_path, &self.dataset.base);
1165 inspection
1166 .verified_files
1167 .delete_paths
1168 .remove(&relative_del_path);
1169 inspection
1170 .referenced_files
1171 .delete_paths
1172 .insert(relative_del_path);
1173 }
1174 is_referenced = true;
1175 }
1176 }
1177 }
1178 }
1179 for index in indexes {
1180 if let Some(base_id) = index.base_id {
1181 let base_path = manifest.base_paths.get(&base_id);
1182 if let Some(base_path) = base_path
1183 && base_path.path == self.dataset.uri
1184 {
1185 let uuid_str = index.uuid.to_string();
1186 inspection.verified_files.index_uuids.remove(&uuid_str);
1187 inspection.referenced_files.index_uuids.insert(uuid_str);
1188 is_referenced = true;
1189 }
1190 }
1191 }
1192 if is_referenced {
1193 inspection
1194 .old_manifests
1195 .retain(|_path, version_number| *version_number != referenced_version);
1196 }
1197
1198 Ok(())
1199 }
1200}
1201
1202fn calculate_duration(scheme: String, rate: u64) -> Duration {
1203 let batch_size = if scheme.to_lowercase().contains("s3") {
1204 S3_DELETE_STREAM_BATCH_SIZE
1205 } else if scheme.to_lowercase().contains("az") {
1206 AZURE_DELETE_STREAM_BATCH_SIZE
1207 } else {
1208 1
1209 };
1210 let effective_rate = rate.max(1);
1211 let path_rate = effective_rate * batch_size;
1212 info!(
1213 "delete_rate_limit enabled: limit {} delete requests/sec",
1214 effective_rate
1215 );
1216 let duration_ns = 1_000_000_000u64.div_ceil(path_rate).max(1);
1218 Duration::from_nanos(duration_ns)
1219}
1220
1221#[derive(Clone, Debug)]
1222pub struct CleanupPolicy {
1223 pub before_timestamp: Option<DateTime<Utc>>,
1225 pub before_version: Option<u64>,
1227 pub delete_unverified: bool,
1229 pub error_if_tagged_old_versions: bool,
1231 pub clean_referenced_branches: bool,
1233 pub delete_rate_limit: Option<u64>,
1239}
1240
1241impl CleanupPolicy {
1242 pub fn should_clean(&self, manifest: &Manifest) -> bool {
1243 let mut should_clean = true;
1244 if let Some(before_timestamp) = self.before_timestamp {
1245 should_clean &= manifest.timestamp() < before_timestamp;
1246 }
1247 if let Some(before_version) = self.before_version {
1248 should_clean &= manifest.version < before_version;
1249 }
1250 should_clean
1251 }
1252}
1253
1254impl Default for CleanupPolicy {
1255 fn default() -> Self {
1256 Self {
1257 before_timestamp: None,
1258 before_version: None,
1259 delete_unverified: false,
1260 error_if_tagged_old_versions: true,
1261 clean_referenced_branches: false,
1262 delete_rate_limit: None,
1263 }
1264 }
1265}
1266
1267#[derive(Default)]
1268pub struct CleanupPolicyBuilder {
1269 policy: CleanupPolicy,
1270}
1271
1272impl CleanupPolicyBuilder {
1273 pub fn clean_referenced_branches(mut self, clean_referenced_branches: bool) -> Self {
1275 self.policy.clean_referenced_branches = clean_referenced_branches;
1276 self
1277 }
1278
1279 pub fn before_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
1281 self.policy.before_timestamp = Some(timestamp);
1282 self
1283 }
1284
1285 pub async fn retain_n_versions(mut self, dataset: &Dataset, n: usize) -> Result<Self> {
1287 let versions = dataset.versions().await?;
1288 self.policy.before_version = if versions.len() <= n {
1289 Some(versions[0].version)
1290 } else {
1291 Some(versions[versions.len() - n].version)
1292 };
1293
1294 Ok(self)
1295 }
1296
1297 pub fn delete_unverified(mut self, delete: bool) -> Self {
1305 self.policy.delete_unverified = delete;
1306 self
1307 }
1308
1309 pub fn error_if_tagged_old_versions(mut self, error: bool) -> Self {
1312 self.policy.error_if_tagged_old_versions = error;
1313 self
1314 }
1315
1316 pub fn delete_rate_limit(mut self, rate: u64) -> Result<Self> {
1326 if rate == 0 {
1327 return Err(Error::Cleanup {
1328 message: format!("delete_rate_limit must be greater than 0, got {}", rate),
1329 });
1330 }
1331 self.policy.delete_rate_limit = Some(rate);
1332 Ok(self)
1333 }
1334
1335 pub fn build(self) -> CleanupPolicy {
1336 self.policy
1337 }
1338}
1339
1340pub async fn cleanup_old_versions(
1351 dataset: &Dataset,
1352 policy: CleanupPolicy,
1353) -> Result<RemovalStats> {
1354 CleanupOperation::new(dataset, policy).execute().await
1355}
1356
1357pub async fn auto_cleanup_hook(
1364 dataset: &Dataset,
1365 manifest: &Manifest,
1366) -> Result<Option<RemovalStats>> {
1367 let policy = build_cleanup_policy(dataset, manifest).await?;
1368 if let Some(policy) = policy {
1369 Ok(Some(dataset.cleanup_with_policy(policy).await?))
1370 } else {
1371 Ok(None)
1372 }
1373}
1374
1375pub async fn cleanup_cascade_branch(
1378 dataset: &Dataset,
1379 manifest: &Manifest,
1380) -> Result<Option<RemovalStats>> {
1381 Ok(
1382 cleanup_cascade_branch_run(dataset, manifest, CleanupAction::Execute, HashSet::new())
1383 .await?
1384 .map(|result| result.stats),
1385 )
1386}
1387
1388async fn cleanup_cascade_branch_run(
1389 dataset: &Dataset,
1390 manifest: &Manifest,
1391 action: CleanupAction,
1392 ignored_manifests: HashSet<Path>,
1393) -> Result<Option<CleanupRunResult>> {
1394 let policy = build_cleanup_policy(dataset, manifest).await?;
1395 if let Some(mut policy) = policy {
1396 policy.clean_referenced_branches = false;
1397 policy.error_if_tagged_old_versions = false;
1398 if action.deletes_files() {
1399 info!(target: TRACE_DATASET_EVENTS, event=DATASET_CLEANING_EVENT, uri=&dataset.uri);
1400 }
1401 let cleanup = CleanupTask::new_with_ignored_manifests(
1402 dataset,
1403 policy,
1404 action,
1405 ignored_manifests,
1406 true,
1407 false,
1408 );
1409 Ok(Some(cleanup.run().await?))
1410 } else {
1411 Ok(None)
1412 }
1413}
1414
1415pub async fn build_cleanup_policy(
1416 dataset: &Dataset,
1417 manifest: &Manifest,
1418) -> Result<Option<CleanupPolicy>> {
1419 if let Some(interval) = manifest.config.get("lance.auto_cleanup.interval") {
1420 let interval: u64 = match interval.parse() {
1421 Ok(i) => i,
1422 Err(e) => {
1423 return Err(Error::Cleanup {
1424 message: format!(
1425 "Error encountered while parsing lance.auto_cleanup.interval as u64: {}",
1426 e
1427 ),
1428 });
1429 }
1430 };
1431
1432 if interval != 0 && !manifest.version.is_multiple_of(interval) {
1433 return Ok(None);
1434 }
1435 } else {
1436 return Ok(None);
1437 }
1438
1439 let mut builder = CleanupPolicyBuilder::default();
1440 if let Some(older_than) = manifest.config.get("lance.auto_cleanup.older_than") {
1441 let std_older_than = match parse_duration(older_than) {
1442 Ok(t) => t,
1443 Err(e) => {
1444 return Err(Error::Cleanup {
1445 message: format!(
1446 "Error encountered while parsing lance.auto_cleanup.older_than as std::time::Duration: {}",
1447 e
1448 ),
1449 });
1450 }
1451 };
1452 let timestamp = utc_now() - TimeDelta::from_std(std_older_than).unwrap_or(TimeDelta::MAX);
1453 builder = builder.before_timestamp(timestamp);
1454 }
1455 if let Some(retain_versions) = manifest.config.get("lance.auto_cleanup.retain_versions") {
1456 let retain_versions: usize = match retain_versions.parse() {
1457 Ok(n) => n,
1458 Err(e) => {
1459 return Err(Error::Cleanup {
1460 message: format!(
1461 "Error encountered while parsing lance.auto_cleanup.retain_versions as u64: {}",
1462 e
1463 ),
1464 });
1465 }
1466 };
1467 builder = builder.retain_n_versions(dataset, retain_versions).await?;
1468 }
1469 if let Some(referenced_branch) = manifest.config.get("lance.auto_cleanup.referenced_branch") {
1470 let clean_referenced: bool = match referenced_branch.parse() {
1471 Ok(b) => b,
1472 Err(e) => {
1473 return Err(Error::Cleanup {
1474 message: format!(
1475 "Error encountered while parsing lance.auto_cleanup.referenced_branch as bool: {}",
1476 e
1477 ),
1478 });
1479 }
1480 };
1481 builder = builder.clean_referenced_branches(clean_referenced);
1483 }
1484 if let Some(delete_rate_limit) = manifest.config.get("lance.auto_cleanup.delete_rate_limit") {
1485 let rate: u64 = match delete_rate_limit.parse() {
1486 Ok(r) => r,
1487 Err(e) => {
1488 return Err(Error::Cleanup {
1489 message: format!(
1490 "Error encountered while parsing lance.auto_cleanup.delete_rate_limit as u64: {}",
1491 e
1492 ),
1493 });
1494 }
1495 };
1496 builder = match builder.delete_rate_limit(rate) {
1497 Ok(b) => b,
1498 Err(e) => return Err(e),
1499 };
1500 }
1501
1502 Ok(Some(builder.build()))
1503}
1504
1505fn tagged_old_versions_cleanup_error(
1506 tags: &HashMap<String, TagContents>,
1507 tagged_old_versions: &HashSet<u64>,
1508) -> Error {
1509 let unreferenced_tags: HashMap<String, u64> = tags
1510 .iter()
1511 .filter_map(|(k, v)| {
1512 if tagged_old_versions.contains(&v.version) {
1513 Some((k.clone(), v.version))
1514 } else {
1515 None
1516 }
1517 })
1518 .collect();
1519
1520 Error::Cleanup {
1521 message: format!(
1522 "{} tagged version(s) have been marked for cleanup. Either set `error_if_tagged_old_versions=false` or delete the following tag(s) to enable cleanup: {:?}",
1523 unreferenced_tags.len(),
1524 unreferenced_tags
1525 ),
1526 }
1527}
1528
1529#[cfg(test)]
1530mod tests {
1531 use std::{
1532 collections::HashMap,
1533 sync::{Arc, Mutex},
1534 };
1535
1536 use super::*;
1537 use crate::blob::{BlobArrayBuilder, blob_field};
1538 use crate::index::DatasetIndexExt;
1539 use crate::{
1540 dataset::transaction::{Operation, Transaction},
1541 dataset::{AutoCleanupParams, ReadParams, WriteMode, WriteParams, builder::DatasetBuilder},
1542 index::vector::VectorIndexParams,
1543 };
1544 use all_asserts::{assert_gt, assert_lt};
1545 use arrow::compute;
1546 use arrow_array::{
1547 Int32Array, RecordBatch, RecordBatchIterator, RecordBatchReader, UInt64Array,
1548 };
1549 use arrow_schema::{DataType, Field, Schema as ArrowSchema};
1550 use datafusion::common::assert_contains;
1551 use lance_core::utils::tempfile::TempStrDir;
1552 use lance_core::utils::testing::{ProxyObjectStore, ProxyObjectStorePolicy};
1553 use lance_index::IndexType;
1554 use lance_io::object_store::{
1555 ObjectStore, ObjectStoreParams, ObjectStoreRegistry, WrappingObjectStore,
1556 };
1557 use lance_linalg::distance::MetricType;
1558 use lance_table::io::commit::RenameCommitHandler;
1559 use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector, some_batch};
1560 use mock_instant::thread_local::MockClock;
1561 use uuid::Uuid;
1562
1563 #[derive(Debug)]
1564 struct MockObjectStore {
1565 policy: Arc<Mutex<ProxyObjectStorePolicy>>,
1566 last_modified_times: Arc<Mutex<HashMap<Path, DateTime<Utc>>>>,
1567 }
1568
1569 impl WrappingObjectStore for MockObjectStore {
1570 fn wrap(
1571 &self,
1572 _storage_prefix: &str,
1573 original: Arc<dyn object_store::ObjectStore>,
1574 ) -> Arc<dyn object_store::ObjectStore> {
1575 Arc::new(ProxyObjectStore::new(original, self.policy.clone()))
1576 }
1577 }
1578
1579 impl MockObjectStore {
1580 pub(crate) fn new() -> Self {
1581 let instance = Self {
1582 policy: Arc::new(Mutex::new(ProxyObjectStorePolicy::new())),
1583 last_modified_times: Arc::new(Mutex::new(HashMap::new())),
1584 };
1585 instance.add_timestamp_policy();
1586 instance
1587 }
1588
1589 fn add_timestamp_policy(&self) {
1590 let mut policy = self.policy.lock().unwrap();
1591 let times_map = self.last_modified_times.clone();
1592 policy.set_before_policy(
1593 "record_file_time",
1594 Arc::new(move |_, path| {
1595 let mut times_map = times_map.lock().unwrap();
1596 times_map.insert(path.clone(), utc_now());
1597 Ok(())
1598 }),
1599 );
1600 let times_map = self.last_modified_times.clone();
1601 policy.set_obj_meta_policy(
1602 "add_recorded_file_time",
1603 Arc::new(move |_, meta| {
1604 let mut meta = meta;
1605 if let Some(recorded) = times_map.lock().unwrap().get(&meta.location) {
1606 meta.last_modified = *recorded;
1607 }
1608 Ok(meta)
1609 }),
1610 );
1611 }
1612 }
1613
1614 #[derive(Debug, PartialEq, Clone, Copy)]
1615 struct FileCounts {
1616 num_data_files: usize,
1617 num_manifest_files: usize,
1618 num_index_files: usize,
1619 num_delete_files: usize,
1620 num_tx_files: usize,
1621 num_bytes: u64,
1622 }
1623
1624 struct MockDatasetFixture {
1625 _tmpdir: TempStrDir,
1628 dataset_path: String,
1629 mock_store: Arc<MockObjectStore>,
1630 }
1631
1632 impl MockDatasetFixture {
1633 fn try_new() -> Result<Self> {
1634 let tmpdir = TempStrDir::default();
1635 let tmpdir_path = tmpdir.as_str();
1636 let path_prefix = if tmpdir_path.starts_with('/') {
1641 ""
1642 } else {
1643 "/"
1644 };
1645 let dataset_path = format!("file-object-store://{path_prefix}{tmpdir_path}/my_db");
1646 Ok(Self {
1647 _tmpdir: tmpdir,
1648 dataset_path,
1649 mock_store: Arc::new(MockObjectStore::new()),
1650 })
1651 }
1652
1653 fn os_params(&self) -> ObjectStoreParams {
1654 ObjectStoreParams {
1655 object_store_wrapper: Some(self.mock_store.clone()),
1656 ..Default::default()
1657 }
1658 }
1659
1660 async fn write_data_impl(
1661 &self,
1662 data: impl RecordBatchReader + Send + 'static,
1663 mode: WriteMode,
1664 ) -> Result<()> {
1665 Dataset::write(
1666 data,
1667 &self.dataset_path,
1668 Some(WriteParams {
1669 store_params: Some(self.os_params()),
1670 commit_handler: Some(Arc::new(RenameCommitHandler)),
1671 mode,
1672 ..Default::default()
1673 }),
1674 )
1675 .await?;
1676 Ok(())
1677 }
1678
1679 async fn write_some_data_impl(&self, mode: WriteMode) -> Result<()> {
1680 self.write_data_impl(some_batch(), mode).await?;
1681 Ok(())
1682 }
1683
1684 async fn create_some_data(&self) -> Result<()> {
1685 self.write_some_data_impl(WriteMode::Create).await
1686 }
1687
1688 async fn create_some_data_with_auto_cleanup(&self) -> Result<()> {
1691 Dataset::write(
1692 some_batch(),
1693 &self.dataset_path,
1694 Some(WriteParams {
1695 store_params: Some(self.os_params()),
1696 commit_handler: Some(Arc::new(RenameCommitHandler)),
1697 mode: WriteMode::Create,
1698 auto_cleanup: Some(AutoCleanupParams::default()),
1699 ..Default::default()
1700 }),
1701 )
1702 .await?;
1703 Ok(())
1704 }
1705
1706 async fn overwrite_some_data(&self) -> Result<()> {
1707 self.write_some_data_impl(WriteMode::Overwrite).await
1708 }
1709
1710 async fn append_some_data(&self) -> Result<()> {
1711 self.write_some_data_impl(WriteMode::Append).await
1712 }
1713
1714 async fn create_with_data(
1715 &self,
1716 data: impl RecordBatchReader + Send + 'static,
1717 ) -> Result<()> {
1718 self.write_data_impl(data, WriteMode::Create).await
1719 }
1720
1721 async fn append_data(&self, data: impl RecordBatchReader + Send + 'static) -> Result<()> {
1722 self.write_data_impl(data, WriteMode::Append).await
1723 }
1724
1725 async fn overwrite_data(
1726 &self,
1727 data: impl RecordBatchReader + Send + 'static,
1728 ) -> Result<()> {
1729 self.write_data_impl(data, WriteMode::Overwrite).await
1730 }
1731
1732 async fn delete_data(&self, predicate: &str) -> Result<()> {
1733 let mut db = self.open().await?;
1734 db.delete(predicate).await?;
1735 Ok(())
1736 }
1737
1738 async fn create_some_index(&self) -> Result<()> {
1739 let mut db = self.open().await?;
1740 let index_params = Box::new(VectorIndexParams::ivf_pq(2, 8, 2, MetricType::L2, 5));
1741 db.create_index(
1742 &["indexable"],
1743 IndexType::Vector,
1744 Some("some_index".to_owned()),
1745 &*index_params,
1746 false,
1747 )
1748 .await?;
1749 Ok(())
1750 }
1751
1752 fn block_commits(&mut self) {
1753 let mut policy = self.mock_store.policy.lock().unwrap();
1754 policy.set_before_policy(
1755 "block_commit",
1756 Arc::new(|op, _| -> Result<()> {
1757 if op.contains("copy") || op.contains("rename") {
1758 return Err(Error::internal("Commit blocked".to_string()));
1759 }
1760 Ok(())
1761 }),
1762 );
1763 }
1764
1765 fn block_delete_manifest(&mut self) {
1766 let mut policy = self.mock_store.policy.lock().unwrap();
1767 policy.set_before_policy(
1768 "block_delete_manifest",
1769 Arc::new(|op, path| -> Result<()> {
1770 if op.contains("delete") && path.extension() == Some("manifest") {
1771 Err(Error::internal("Delete manifest blocked".to_string()))
1772 } else {
1773 Ok(())
1774 }
1775 }),
1776 );
1777 }
1778
1779 fn unblock_delete_manifest(&mut self) {
1780 let mut policy = self.mock_store.policy.lock().unwrap();
1781 policy.clear_before_policy("block_delete_manifest");
1782 }
1783
1784 async fn run_cleanup(&self, before: DateTime<Utc>) -> Result<RemovalStats> {
1785 let db = self.open().await?;
1786 cleanup_old_versions(
1787 &db,
1788 CleanupPolicyBuilder::default()
1789 .before_timestamp(before)
1790 .build(),
1791 )
1792 .await
1793 }
1794
1795 async fn run_cleanup_with_policy(&self, policy: CleanupPolicy) -> Result<RemovalStats> {
1796 let db = self.open().await?;
1797 cleanup_old_versions(&db, policy).await
1798 }
1799
1800 async fn explain_cleanup_with_policy(
1801 &self,
1802 policy: CleanupPolicy,
1803 ) -> Result<CleanupExplanation> {
1804 let db = self.open().await?;
1805 db.cleanup(policy).explain().await
1806 }
1807
1808 async fn run_cleanup_with_override(
1809 &self,
1810 before: DateTime<Utc>,
1811 delete_unverified: Option<bool>,
1812 error_if_tagged_old_versions: Option<bool>,
1813 ) -> Result<RemovalStats> {
1814 let db = self.open().await?;
1815 cleanup_old_versions(
1816 &db,
1817 CleanupPolicyBuilder::default()
1818 .before_timestamp(before)
1819 .delete_unverified(delete_unverified.unwrap_or(false))
1820 .error_if_tagged_old_versions(error_if_tagged_old_versions.unwrap_or(true))
1821 .build(),
1822 )
1823 .await
1824 }
1825
1826 async fn open(&self) -> Result<Box<Dataset>> {
1827 let ds = DatasetBuilder::from_uri(&self.dataset_path)
1828 .with_read_params(ReadParams {
1829 store_options: Some(self.os_params()),
1830 ..Default::default()
1831 })
1832 .load()
1833 .await?;
1834 Ok(Box::new(ds))
1835 }
1836
1837 async fn load(&self) -> Result<Dataset> {
1839 self.load_dataset(&self.dataset_path).await
1840 }
1841
1842 async fn load_dataset(&self, uri: &str) -> Result<Dataset> {
1844 DatasetBuilder::from_uri(uri)
1845 .with_read_params(ReadParams {
1846 store_options: Some(self.os_params()),
1847 ..Default::default()
1848 })
1849 .load()
1850 .await
1851 }
1852
1853 async fn create_branch_and_load<V: Into<crate::dataset::refs::Ref>>(
1855 &self,
1856 from_dataset: &mut Dataset,
1857 branch_name: &str,
1858 source_ref: V,
1859 ) -> Result<Dataset> {
1860 let branch_ds = from_dataset
1861 .create_branch(branch_name, source_ref, Some(self.os_params()))
1862 .await?;
1863 self.load_dataset(&branch_ds.uri).await
1864 }
1865
1866 async fn count_files(&self) -> Result<FileCounts> {
1867 let registry = Arc::new(ObjectStoreRegistry::default());
1868 let (os, path) =
1869 ObjectStore::from_uri_and_params(registry, &self.dataset_path, &self.os_params())
1870 .await?;
1871 let mut file_stream = os.read_dir_all(&path, None);
1872 let mut file_count = FileCounts {
1873 num_data_files: 0,
1874 num_delete_files: 0,
1875 num_index_files: 0,
1876 num_manifest_files: 0,
1877 num_tx_files: 0,
1878 num_bytes: 0,
1879 };
1880 while let Some(path) = file_stream.try_next().await? {
1881 file_count.num_bytes += path.size;
1882 match path.location.extension() {
1883 Some("lance") => file_count.num_data_files += 1,
1884 Some("manifest") => file_count.num_manifest_files += 1,
1885 Some("arrow") | Some("bin") => file_count.num_delete_files += 1,
1886 Some("idx") => file_count.num_index_files += 1,
1887 Some("txn") => file_count.num_tx_files += 1,
1888 _ => (),
1889 }
1890 }
1891 Ok(file_count)
1892 }
1893
1894 async fn count_blob_files(&self) -> Result<usize> {
1895 let registry = Arc::new(ObjectStoreRegistry::default());
1896 let (os, path) =
1897 ObjectStore::from_uri_and_params(registry, &self.dataset_path, &self.os_params())
1898 .await?;
1899 let mut file_stream = os.read_dir_all(&path, None);
1900 let mut blob_count = 0usize;
1901 while let Some(path) = file_stream.try_next().await? {
1902 if path.location.extension() == Some("blob") {
1903 blob_count += 1;
1904 }
1905 }
1906 Ok(blob_count)
1907 }
1908
1909 async fn count_rows(&self) -> Result<usize> {
1910 let db = self.open().await?;
1911 let count = db.count_rows(None).await?;
1912 Ok(count)
1913 }
1914 }
1915
1916 async fn write_dummy_index_artifact(dataset: &Dataset, uuid: Uuid) -> Result<()> {
1917 let index_dir = dataset.indices_dir().join(uuid.to_string());
1918 dataset
1919 .object_store
1920 .as_ref()
1921 .put(&index_dir.clone().join("index.idx"), b"idx")
1922 .await?;
1923 dataset
1924 .object_store
1925 .as_ref()
1926 .put(&index_dir.clone().join("auxiliary.idx"), b"aux")
1927 .await?;
1928 Ok(())
1929 }
1930
1931 async fn write_dummy_staging_partial(
1932 dataset: &Dataset,
1933 staging_uuid: Uuid,
1934 shard_uuid: Uuid,
1935 ) -> Result<()> {
1936 let shard_dir = dataset
1937 .indices_dir()
1938 .join(staging_uuid.to_string())
1939 .join(format!("partial_{}", shard_uuid));
1940 dataset
1941 .object_store
1942 .as_ref()
1943 .put(&shard_dir.clone().join("index.idx"), b"idx")
1944 .await?;
1945 dataset
1946 .object_store
1947 .as_ref()
1948 .put(&shard_dir.clone().join("auxiliary.idx"), b"aux")
1949 .await?;
1950 Ok(())
1951 }
1952
1953 fn dummy_index_metadata(
1954 dataset: &Dataset,
1955 field_id: i32,
1956 uuid: Uuid,
1957 fragment_bitmap: impl IntoIterator<Item = u32>,
1958 ) -> IndexMetadata {
1959 IndexMetadata {
1960 uuid,
1961 name: "some_index".to_string(),
1962 fields: vec![field_id],
1963 dataset_version: dataset.version().version,
1964 fragment_bitmap: Some(fragment_bitmap.into_iter().collect()),
1965 index_details: None,
1966 index_version: IndexType::Vector.version(),
1967 created_at: None,
1968 base_id: None,
1969 files: None,
1970 }
1971 }
1972
1973 fn blob_v2_batch(blob_len: usize) -> Box<dyn RecordBatchReader + Send> {
1974 let mut blobs = BlobArrayBuilder::new(1);
1975 blobs.push_bytes(vec![0u8; blob_len]).unwrap();
1976
1977 let schema = Arc::new(ArrowSchema::new(vec![
1978 Field::new("id", DataType::Int32, false),
1979 blob_field("blob", true),
1980 ]));
1981
1982 let batch = RecordBatch::try_new(
1983 schema.clone(),
1984 vec![Arc::new(Int32Array::from(vec![1])), blobs.finish().unwrap()],
1985 )
1986 .unwrap();
1987
1988 Box::new(RecordBatchIterator::new(
1989 vec![Ok(batch)].into_iter(),
1990 schema,
1991 ))
1992 }
1993
1994 #[tokio::test]
1995 async fn cleanup_unreferenced_data_files() {
1996 let fixture = MockDatasetFixture::try_new().unwrap();
2000 fixture.create_some_data().await.unwrap();
2001 fixture.overwrite_some_data().await.unwrap();
2002
2003 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2004
2005 let before_count = fixture.count_files().await.unwrap();
2006
2007 let removed = fixture
2008 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2009 .await
2010 .unwrap();
2011
2012 let after_count = fixture.count_files().await.unwrap();
2013 assert_eq!(removed.old_versions, 1);
2014 assert_eq!(removed.data_files_removed, 1);
2015 assert_eq!(
2016 removed.bytes_removed,
2017 before_count.num_bytes - after_count.num_bytes
2018 );
2019
2020 assert_lt!(after_count.num_data_files, before_count.num_data_files);
2022 assert_lt!(
2024 after_count.num_manifest_files,
2025 before_count.num_manifest_files
2026 );
2027 assert_lt!(after_count.num_tx_files, before_count.num_tx_files);
2028
2029 assert_gt!(after_count.num_manifest_files, 0);
2030 assert_gt!(after_count.num_data_files, 0);
2031 assert_gt!(after_count.num_tx_files, 0);
2033 }
2034
2035 #[tokio::test]
2036 async fn explain_cleanup_does_not_delete_files() {
2037 let fixture = MockDatasetFixture::try_new().unwrap();
2038 fixture.create_some_data().await.unwrap();
2039 MockClock::set_system_time(TimeDelta::try_seconds(1).unwrap().to_std().unwrap());
2040 fixture.overwrite_some_data().await.unwrap();
2041
2042 let before_count = fixture.count_files().await.unwrap();
2043 let policy = CleanupPolicyBuilder::default()
2044 .before_timestamp(utc_now())
2045 .build();
2046
2047 let explanation = fixture
2048 .explain_cleanup_with_policy(policy.clone())
2049 .await
2050 .unwrap();
2051 let after_preview_count = fixture.count_files().await.unwrap();
2052
2053 assert_eq!(before_count, after_preview_count);
2055 assert_eq!(explanation.read_version, 2);
2056 assert_eq!(explanation.stats.old_versions, 1);
2057 assert_eq!(explanation.stats.data_files_removed, 1);
2058 assert_eq!(explanation.stats.transaction_files_removed, 1);
2059 assert_gt!(explanation.stats.bytes_removed, 0);
2060 assert!(!explanation.candidate_files.is_empty());
2061 assert!(!explanation.candidate_files_truncated);
2062
2063 let removed = fixture.run_cleanup_with_policy(policy).await.unwrap();
2066 let after_cleanup_count = fixture.count_files().await.unwrap();
2067
2068 assert_eq!(
2069 removed.bytes_removed,
2070 before_count.num_bytes - after_cleanup_count.num_bytes
2071 );
2072 assert_eq!(removed.old_versions, explanation.stats.old_versions);
2073 assert_eq!(
2074 removed.data_files_removed,
2075 explanation.stats.data_files_removed
2076 );
2077 assert_eq!(removed.bytes_removed, explanation.stats.bytes_removed);
2078 }
2079
2080 #[tokio::test]
2081 async fn cleanup_blob_v2_sidecar_files() {
2082 let fixture = MockDatasetFixture::try_new().unwrap();
2083
2084 Dataset::write(
2086 blob_v2_batch(100 * 1024),
2087 &fixture.dataset_path,
2088 Some(WriteParams {
2089 store_params: Some(fixture.os_params()),
2090 commit_handler: Some(Arc::new(RenameCommitHandler)),
2091 mode: WriteMode::Create,
2092 data_storage_version: Some(lance_file::version::LanceFileVersion::V2_2),
2093 ..Default::default()
2094 }),
2095 )
2096 .await
2097 .unwrap();
2098 assert_gt!(fixture.count_blob_files().await.unwrap(), 0);
2099
2100 Dataset::write(
2102 blob_v2_batch(1024),
2103 &fixture.dataset_path,
2104 Some(WriteParams {
2105 store_params: Some(fixture.os_params()),
2106 commit_handler: Some(Arc::new(RenameCommitHandler)),
2107 mode: WriteMode::Overwrite,
2108 data_storage_version: Some(lance_file::version::LanceFileVersion::V2_2),
2109 ..Default::default()
2110 }),
2111 )
2112 .await
2113 .unwrap();
2114
2115 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2117
2118 fixture
2119 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2120 .await
2121 .unwrap();
2122
2123 assert_eq!(fixture.count_blob_files().await.unwrap(), 0);
2124 }
2125
2126 #[tokio::test]
2127 async fn cleanup_recent_blob_v2_sidecar_files_when_verified() {
2128 let fixture = MockDatasetFixture::try_new().unwrap();
2129
2130 Dataset::write(
2131 blob_v2_batch(100 * 1024),
2132 &fixture.dataset_path,
2133 Some(WriteParams {
2134 store_params: Some(fixture.os_params()),
2135 commit_handler: Some(Arc::new(RenameCommitHandler)),
2136 mode: WriteMode::Create,
2137 data_storage_version: Some(lance_file::version::LanceFileVersion::V2_2),
2138 ..Default::default()
2139 }),
2140 )
2141 .await
2142 .unwrap();
2143
2144 Dataset::write(
2145 blob_v2_batch(1024),
2146 &fixture.dataset_path,
2147 Some(WriteParams {
2148 store_params: Some(fixture.os_params()),
2149 commit_handler: Some(Arc::new(RenameCommitHandler)),
2150 mode: WriteMode::Overwrite,
2151 data_storage_version: Some(lance_file::version::LanceFileVersion::V2_2),
2152 ..Default::default()
2153 }),
2154 )
2155 .await
2156 .unwrap();
2157
2158 fixture
2161 .run_cleanup(utc_now() + TimeDelta::seconds(1))
2162 .await
2163 .unwrap();
2164
2165 assert_eq!(fixture.count_blob_files().await.unwrap(), 0);
2166 }
2167
2168 #[tokio::test]
2169 async fn do_not_cleanup_newer_data() {
2170 let fixture = MockDatasetFixture::try_new().unwrap();
2173 fixture.create_some_data().await.unwrap();
2174 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2175 fixture.append_some_data().await.unwrap();
2176 fixture.append_some_data().await.unwrap();
2177
2178 let before_count = fixture.count_files().await.unwrap();
2179
2180 assert_eq!(before_count.num_data_files, 3);
2182 assert_eq!(before_count.num_manifest_files, 3);
2183
2184 let before = utc_now() - TimeDelta::try_days(7).unwrap();
2185 let removed = fixture.run_cleanup(before).await.unwrap();
2186
2187 let after_count = fixture.count_files().await.unwrap();
2188
2189 assert_eq!(removed.old_versions, 1);
2190 assert_eq!(
2191 removed.bytes_removed,
2192 before_count.num_bytes - after_count.num_bytes
2193 );
2194
2195 assert_eq!(after_count.num_data_files, 3);
2198 assert_eq!(after_count.num_manifest_files, 2);
2200 assert_eq!(after_count.num_tx_files, 2);
2201 }
2202
2203 #[tokio::test]
2204 async fn cleanup_error_when_tagged_old_versions() {
2205 let fixture = MockDatasetFixture::try_new().unwrap();
2210 fixture.create_some_data().await.unwrap();
2211 fixture.overwrite_some_data().await.unwrap();
2212 fixture.overwrite_some_data().await.unwrap();
2213
2214 let dataset = *(fixture.open().await.unwrap());
2215
2216 dataset.tags().create("old-tag", 1).await.unwrap();
2217 dataset.tags().create("another-old-tag", 2).await.unwrap();
2218
2219 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2220
2221 let removed = fixture
2222 .run_cleanup(utc_now() - TimeDelta::try_days(20).unwrap())
2223 .await
2224 .unwrap();
2225 assert_eq!(removed.old_versions, 0);
2226
2227 let mut cleanup_error = fixture
2228 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2229 .await
2230 .err()
2231 .unwrap();
2232 assert_contains!(
2233 cleanup_error.to_string(),
2234 "Cleanup error: 2 tagged version(s) have been marked for cleanup. Either set `error_if_tagged_old_versions=false` or delete the following tag(s) to enable cleanup:"
2235 );
2236
2237 dataset.tags().delete("old-tag").await.unwrap();
2238
2239 cleanup_error = fixture
2240 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2241 .await
2242 .err()
2243 .unwrap();
2244 assert_contains!(
2245 cleanup_error.to_string(),
2246 "Cleanup error: 1 tagged version(s) have been marked for cleanup. Either set `error_if_tagged_old_versions=false` or delete the following tag(s) to enable cleanup:"
2247 );
2248
2249 dataset.tags().delete("another-old-tag").await.unwrap();
2250
2251 let removed = fixture
2252 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2253 .await
2254 .unwrap();
2255 assert_eq!(removed.old_versions, 2);
2256 }
2257
2258 #[tokio::test]
2259 async fn cleanup_around_tagged_old_versions() {
2260 let fixture = MockDatasetFixture::try_new().unwrap();
2265 fixture.create_some_data().await.unwrap();
2266 fixture.overwrite_some_data().await.unwrap();
2267 fixture.overwrite_some_data().await.unwrap();
2268
2269 let dataset = *(fixture.open().await.unwrap());
2270
2271 dataset.tags().create("old-tag", 1).await.unwrap();
2272 dataset.tags().create("another-old-tag", 2).await.unwrap();
2273 dataset.tags().create("tag-latest", 3).await.unwrap();
2274
2275 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2276
2277 let mut removed = fixture
2278 .run_cleanup_with_override(
2279 utc_now() - TimeDelta::try_days(8).unwrap(),
2280 None,
2281 Some(false),
2282 )
2283 .await
2284 .unwrap();
2285
2286 assert_eq!(removed.old_versions, 0);
2287
2288 dataset.tags().delete("old-tag").await.unwrap();
2289
2290 removed = fixture
2291 .run_cleanup_with_override(
2292 utc_now() - TimeDelta::try_days(8).unwrap(),
2293 None,
2294 Some(false),
2295 )
2296 .await
2297 .unwrap();
2298 assert_eq!(removed.old_versions, 1);
2299
2300 dataset.tags().delete("another-old-tag").await.unwrap();
2301
2302 removed = fixture
2303 .run_cleanup_with_override(
2304 utc_now() - TimeDelta::try_days(8).unwrap(),
2305 None,
2306 Some(false),
2307 )
2308 .await
2309 .unwrap();
2310
2311 assert_eq!(removed.old_versions, 1);
2312 }
2313
2314 async fn check_num_files(fixture: &MockDatasetFixture, num_expected_files: usize) {
2316 let file_count = fixture.count_files().await.unwrap();
2317
2318 assert_eq!(file_count.num_data_files, num_expected_files);
2319 assert_eq!(file_count.num_manifest_files, num_expected_files);
2320 assert_eq!(file_count.num_tx_files, num_expected_files);
2321 }
2322
2323 #[tokio::test]
2324 async fn auto_cleanup_old_versions() {
2325 let fixture = MockDatasetFixture::try_new().unwrap();
2334
2335 fixture.create_some_data_with_auto_cleanup().await.unwrap();
2336
2337 let dataset_config = &fixture.open().await.unwrap().manifest.config;
2338 let cleanup_interval: usize = dataset_config
2339 .get("lance.auto_cleanup.interval")
2340 .unwrap()
2341 .parse()
2342 .unwrap();
2343
2344 let cleanup_older_than = TimeDelta::from_std(
2345 parse_duration(dataset_config.get("lance.auto_cleanup.older_than").unwrap()).unwrap(),
2346 )
2347 .unwrap();
2348
2349 for num_expected_files in 2..2 * cleanup_interval {
2352 fixture.overwrite_some_data().await.unwrap();
2353 check_num_files(&fixture, num_expected_files).await;
2354 }
2355
2356 MockClock::set_system_time(
2358 (cleanup_older_than + TimeDelta::minutes(1))
2359 .to_std()
2360 .unwrap(),
2361 );
2362
2363 for num_expected_files in 2..cleanup_interval {
2366 fixture.overwrite_some_data().await.unwrap();
2367 check_num_files(&fixture, num_expected_files).await;
2368 }
2369
2370 let mut dataset = *(fixture.open().await.unwrap());
2372 let mut new_autoclean_params = HashMap::new();
2373
2374 let new_cleanup_older_than_str = "1month 2days 2h 42min 6sec";
2375 let new_cleanup_older_than =
2376 TimeDelta::from_std(parse_duration(new_cleanup_older_than_str).unwrap()).unwrap();
2377 new_autoclean_params.insert(
2378 "lance.auto_cleanup.older_than".to_string(),
2379 new_cleanup_older_than_str.to_string(),
2380 );
2381
2382 let new_cleanup_interval = 5;
2383 new_autoclean_params.insert(
2384 "lance.auto_cleanup.interval".to_string(),
2385 new_cleanup_interval.to_string(),
2386 );
2387
2388 let config_updates = new_autoclean_params
2390 .into_iter()
2391 .map(|(k, v)| (k, Some(v)))
2392 .collect::<HashMap<String, Option<String>>>();
2393 dataset.update_config(config_updates).await.unwrap();
2394
2395 MockClock::set_system_time(
2397 (cleanup_older_than + new_cleanup_older_than + TimeDelta::minutes(2))
2398 .to_std()
2399 .unwrap(),
2400 );
2401
2402 fixture.overwrite_some_data().await.unwrap();
2403
2404 for num_expected_files in 2..new_cleanup_interval {
2405 fixture.overwrite_some_data().await.unwrap();
2406 check_num_files(&fixture, num_expected_files).await;
2407 }
2408 }
2409
2410 #[tokio::test]
2411 async fn test_auto_cleanup_interval_zero() {
2412 let fixture = MockDatasetFixture::try_new().unwrap();
2413
2414 fixture.create_some_data().await.unwrap();
2415 fixture.overwrite_some_data().await.unwrap();
2416 fixture.overwrite_some_data().await.unwrap();
2417 check_num_files(&fixture, 3).await;
2418
2419 let mut dataset = fixture.open().await.unwrap();
2420 let mut config_updates = HashMap::new();
2421 config_updates.insert(
2422 "lance.auto_cleanup.interval".to_string(),
2423 Some("0".to_string()),
2424 );
2425 config_updates.insert(
2426 "lance.auto_cleanup.retain_versions".to_string(),
2427 Some("1".to_string()),
2428 );
2429 dataset
2430 .update_config(config_updates)
2431 .replace()
2432 .await
2433 .unwrap();
2434
2435 fixture.overwrite_some_data().await.unwrap();
2436 fixture.overwrite_some_data().await.unwrap();
2437 check_num_files(&fixture, 2).await;
2439
2440 fixture.overwrite_some_data().await.unwrap();
2441 check_num_files(&fixture, 2).await;
2442 }
2443
2444 #[tokio::test]
2445 async fn cleanup_recent_verified_files() {
2446 let fixture = MockDatasetFixture::try_new().unwrap();
2447 fixture.create_some_data().await.unwrap();
2448 MockClock::set_system_time(TimeDelta::try_seconds(1).unwrap().to_std().unwrap());
2449 fixture.overwrite_some_data().await.unwrap();
2450
2451 let before_count = fixture.count_files().await.unwrap();
2452 assert_eq!(before_count.num_data_files, 2);
2453 assert_eq!(before_count.num_manifest_files, 2);
2454
2455 let before = utc_now();
2458 let removed = fixture.run_cleanup(before).await.unwrap();
2459
2460 let after_count = fixture.count_files().await.unwrap();
2461 assert_eq!(removed.old_versions, 1);
2462 assert_eq!(
2463 removed.bytes_removed,
2464 before_count.num_bytes - after_count.num_bytes
2465 );
2466
2467 assert_eq!(after_count.num_data_files, 1);
2468 assert_eq!(after_count.num_manifest_files, 1);
2469 }
2470
2471 #[tokio::test]
2472 async fn dont_cleanup_recent_unverified_files() {
2473 for (override_opt, old_files) in [
2474 (Some(false), false), (Some(true), false), (None, true), (None, false), ] {
2479 MockClock::set_system_time(std::time::Duration::from_secs(0));
2480 let mut fixture = MockDatasetFixture::try_new().unwrap();
2481 fixture.create_some_data().await.unwrap();
2482 fixture.block_commits();
2483 assert!(fixture.append_some_data().await.is_err());
2484
2485 let age = if old_files {
2486 TimeDelta::try_days(UNVERIFIED_THRESHOLD_DAYS + 1).unwrap()
2487 } else {
2488 TimeDelta::try_days(UNVERIFIED_THRESHOLD_DAYS - 1).unwrap()
2489 };
2490 MockClock::set_system_time(age.to_std().unwrap());
2491
2492 let before_count = fixture.count_files().await.unwrap();
2497 assert_eq!(before_count.num_data_files, 2);
2498 assert_eq!(before_count.num_manifest_files, 1);
2499
2500 let before = utc_now();
2501 let removed = fixture
2502 .run_cleanup_with_override(before, override_opt, None)
2503 .await
2504 .unwrap();
2505
2506 let should_delete = override_opt.unwrap_or(false) || old_files;
2507
2508 let after_count = fixture.count_files().await.unwrap();
2509 assert_eq!(removed.old_versions, 0);
2510 assert_eq!(
2511 removed.bytes_removed,
2512 before_count.num_bytes - after_count.num_bytes
2513 );
2514
2515 if should_delete {
2516 assert_gt!(removed.bytes_removed, 0);
2517 } else {
2518 assert_eq!(removed.bytes_removed, 0);
2519 }
2520 }
2521 }
2522
2523 #[tokio::test]
2524 async fn cleanup_old_index() {
2525 let fixture = MockDatasetFixture::try_new().unwrap();
2526 fixture.create_some_data().await.unwrap();
2527 fixture.create_some_index().await.unwrap();
2528 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2529 fixture.overwrite_some_data().await.unwrap();
2530
2531 let before_count = fixture.count_files().await.unwrap();
2532 assert_eq!(before_count.num_index_files, 2);
2534 assert_eq!(before_count.num_data_files, 2);
2536 assert_eq!(before_count.num_manifest_files, 3);
2538
2539 let before = utc_now() - TimeDelta::try_days(8).unwrap();
2540 let removed = fixture.run_cleanup(before).await.unwrap();
2541
2542 let after_count = fixture.count_files().await.unwrap();
2543 assert_eq!(removed.old_versions, 2);
2544 assert_eq!(
2545 removed.bytes_removed,
2546 before_count.num_bytes - after_count.num_bytes
2547 );
2548
2549 assert_eq!(after_count.num_index_files, 0);
2550 assert_eq!(after_count.num_data_files, 1);
2551 assert_eq!(after_count.num_manifest_files, 1);
2552 assert_eq!(after_count.num_tx_files, 1);
2553 }
2554
2555 #[tokio::test]
2556 async fn clean_old_delete_files() {
2557 let fixture = MockDatasetFixture::try_new().unwrap();
2558 let mut data_gen = BatchGenerator::new().col(Box::new(
2559 IncrementingInt32::new().named("filter_me".to_owned()),
2560 ));
2561
2562 fixture.create_with_data(data_gen.batch(16)).await.unwrap();
2563 fixture.append_data(data_gen.batch(16)).await.unwrap();
2564 fixture.delete_data("filter_me < 20").await.unwrap();
2567 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2568 fixture.overwrite_data(data_gen.batch(16)).await.unwrap();
2569 fixture.delete_data("filter_me >= 40").await.unwrap();
2571
2572 let before_count = fixture.count_files().await.unwrap();
2573 assert_eq!(before_count.num_data_files, 3);
2574 assert_eq!(before_count.num_delete_files, 2);
2575 assert_eq!(before_count.num_manifest_files, 5);
2576
2577 let before = utc_now() - TimeDelta::try_days(8).unwrap();
2578 let removed = fixture.run_cleanup(before).await.unwrap();
2579
2580 let after_count = fixture.count_files().await.unwrap();
2581 assert_eq!(removed.old_versions, 3);
2582 assert_eq!(
2583 removed.bytes_removed,
2584 before_count.num_bytes - after_count.num_bytes
2585 );
2586
2587 assert_eq!(after_count.num_data_files, 1);
2588 assert_eq!(after_count.num_delete_files, 1);
2589 assert_eq!(after_count.num_manifest_files, 2);
2590 assert_eq!(after_count.num_tx_files, 2);
2591
2592 let row_count_after = fixture.count_rows().await.unwrap();
2594 assert_eq!(row_count_after, 8);
2595 }
2596
2597 #[tokio::test]
2598 async fn cleanup_collects_removed_file_metrics() {
2599 let fixture = MockDatasetFixture::try_new().unwrap();
2600 let row_count = 512;
2601 let mut data_gen = BatchGenerator::new()
2602 .col(Box::new(
2603 IncrementingInt32::new().named("filter_me".to_owned()),
2604 ))
2605 .col(Box::new(RandomVector::new().named("indexable".to_owned())));
2606
2607 fixture
2608 .create_with_data(data_gen.batch(row_count))
2609 .await
2610 .unwrap();
2611 fixture
2612 .append_data(data_gen.batch(row_count))
2613 .await
2614 .unwrap();
2615 fixture.create_some_index().await.unwrap();
2616 fixture.delete_data("filter_me < 20").await.unwrap();
2617 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2618 fixture
2619 .overwrite_data(data_gen.batch(row_count))
2620 .await
2621 .unwrap();
2622 fixture.delete_data("filter_me >= 40").await.unwrap();
2623
2624 let before_count = fixture.count_files().await.unwrap();
2625 let removed = fixture
2626 .run_cleanup(utc_now() - TimeDelta::try_days(8).unwrap())
2627 .await
2628 .unwrap();
2629 let after_count = fixture.count_files().await.unwrap();
2630
2631 let data_files_removed = (before_count.num_data_files - after_count.num_data_files) as u64;
2632 let transaction_files_removed =
2633 (before_count.num_tx_files - after_count.num_tx_files) as u64;
2634 let index_files_removed =
2635 (before_count.num_index_files - after_count.num_index_files) as u64;
2636 let deletion_files_removed =
2637 (before_count.num_delete_files - after_count.num_delete_files) as u64;
2638
2639 assert_eq!(removed.data_files_removed, data_files_removed);
2640 assert_eq!(removed.transaction_files_removed, transaction_files_removed);
2641 assert_eq!(removed.index_files_removed, index_files_removed);
2642 assert_eq!(removed.deletion_files_removed, deletion_files_removed);
2643 assert_gt!(removed.data_files_removed, 0);
2644 assert_gt!(removed.transaction_files_removed, 0);
2645 assert_gt!(removed.index_files_removed, 0);
2646 assert_gt!(removed.deletion_files_removed, 0);
2647 }
2648
2649 #[tokio::test]
2650 async fn dont_clean_index_data_files() {
2651 let fixture = MockDatasetFixture::try_new().unwrap();
2655 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2656 fixture.create_some_data().await.unwrap();
2657 fixture.create_some_index().await.unwrap();
2658
2659 let before_count = fixture.count_files().await.unwrap();
2660 let before = utc_now() - TimeDelta::try_days(8).unwrap();
2661 let removed = fixture.run_cleanup(before).await.unwrap();
2662 assert_eq!(removed.old_versions, 0);
2663 assert_eq!(removed.bytes_removed, 0);
2664
2665 let after_count = fixture.count_files().await.unwrap();
2666
2667 assert_eq!(before_count, after_count);
2668 }
2669
2670 #[tokio::test]
2671 async fn cleanup_old_replaced_segment_keeps_still_referenced_segments() {
2672 let fixture = MockDatasetFixture::try_new().unwrap();
2673 fixture.create_some_data().await.unwrap();
2674
2675 let mut dataset = fixture.open().await.unwrap();
2676 let field_id = dataset.schema().field("indexable").unwrap().id;
2677
2678 let seg_a = Uuid::new_v4();
2679 let seg_b = Uuid::new_v4();
2680 write_dummy_index_artifact(&dataset, seg_a).await.unwrap();
2681 write_dummy_index_artifact(&dataset, seg_b).await.unwrap();
2682
2683 let index_a = dummy_index_metadata(&dataset, field_id, seg_a, [0_u32]);
2684 let index_b = dummy_index_metadata(&dataset, field_id, seg_b, [1_u32]);
2685 let initial_tx = Transaction::new(
2686 dataset.manifest.version,
2687 Operation::CreateIndex {
2688 new_indices: vec![index_a.clone(), index_b.clone()],
2689 removed_indices: vec![],
2690 },
2691 None,
2692 );
2693 dataset
2694 .apply_commit(initial_tx, &Default::default(), &Default::default())
2695 .await
2696 .unwrap();
2697
2698 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2699
2700 let seg_c = Uuid::new_v4();
2701 write_dummy_index_artifact(&dataset, seg_c).await.unwrap();
2702 let index_c = dummy_index_metadata(&dataset, field_id, seg_c, [2_u32]);
2703 let replace_tx = Transaction::new(
2704 dataset.manifest.version,
2705 Operation::CreateIndex {
2706 new_indices: vec![index_c.clone()],
2707 removed_indices: vec![index_a.clone()],
2708 },
2709 None,
2710 );
2711 dataset
2712 .apply_commit(replace_tx, &Default::default(), &Default::default())
2713 .await
2714 .unwrap();
2715
2716 let removed = fixture
2717 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2718 .await
2719 .unwrap();
2720
2721 assert_eq!(removed.index_files_removed, 2);
2722 assert!(
2723 !dataset
2724 .object_store
2725 .as_ref()
2726 .exists(
2727 &dataset
2728 .indices_dir()
2729 .clone()
2730 .join(seg_a.to_string())
2731 .join("index.idx")
2732 )
2733 .await
2734 .unwrap()
2735 );
2736 assert!(
2737 dataset
2738 .object_store
2739 .as_ref()
2740 .exists(
2741 &dataset
2742 .indices_dir()
2743 .clone()
2744 .join(seg_b.to_string())
2745 .join("index.idx")
2746 )
2747 .await
2748 .unwrap()
2749 );
2750 assert!(
2751 dataset
2752 .object_store
2753 .as_ref()
2754 .exists(
2755 &dataset
2756 .indices_dir()
2757 .clone()
2758 .join(seg_c.to_string())
2759 .join("index.idx")
2760 )
2761 .await
2762 .unwrap()
2763 );
2764 }
2765
2766 #[tokio::test]
2767 async fn cleanup_old_uncommitted_index_artifacts() {
2768 let fixture = MockDatasetFixture::try_new().unwrap();
2769 fixture.create_some_data().await.unwrap();
2770
2771 let dataset = fixture.open().await.unwrap();
2772 let staging_uuid = Uuid::new_v4();
2773 let shard_uuid = Uuid::new_v4();
2774 let built_segment_uuid = Uuid::new_v4();
2775
2776 write_dummy_staging_partial(&dataset, staging_uuid, shard_uuid)
2777 .await
2778 .unwrap();
2779 write_dummy_index_artifact(&dataset, built_segment_uuid)
2780 .await
2781 .unwrap();
2782
2783 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2784
2785 let removed = fixture
2786 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2787 .await
2788 .unwrap();
2789
2790 assert_eq!(removed.old_versions, 0);
2791 assert_eq!(removed.index_files_removed, 4);
2792 assert!(
2793 !dataset
2794 .object_store
2795 .as_ref()
2796 .exists(
2797 &dataset
2798 .indices_dir()
2799 .clone()
2800 .join(staging_uuid.to_string())
2801 .join(format!("partial_{}", shard_uuid))
2802 .join("index.idx"),
2803 )
2804 .await
2805 .unwrap()
2806 );
2807 assert!(
2808 !dataset
2809 .object_store
2810 .as_ref()
2811 .exists(
2812 &dataset
2813 .indices_dir()
2814 .clone()
2815 .join(built_segment_uuid.to_string())
2816 .join("index.idx"),
2817 )
2818 .await
2819 .unwrap()
2820 );
2821 }
2822
2823 #[tokio::test]
2824 async fn cleanup_failed_commit_data_file() {
2825 let mut fixture = MockDatasetFixture::try_new().unwrap();
2829 fixture.create_some_data().await.unwrap();
2830 fixture.block_commits();
2831 assert!(fixture.append_some_data().await.is_err());
2832 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2833
2834 let before_count = fixture.count_files().await.unwrap();
2835 assert_eq!(before_count.num_data_files, 2);
2838 assert_eq!(before_count.num_manifest_files, 1);
2839 assert_eq!(before_count.num_tx_files, 1);
2841
2842 let removed = fixture
2845 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2846 .await
2847 .unwrap();
2848
2849 let after_count = fixture.count_files().await.unwrap();
2850 assert_eq!(removed.old_versions, 0);
2851 assert_eq!(removed.data_files_removed, 1);
2852 assert_eq!(
2853 removed.bytes_removed,
2854 before_count.num_bytes - after_count.num_bytes
2855 );
2856
2857 assert_eq!(after_count.num_data_files, 1);
2858 assert_eq!(after_count.num_manifest_files, 1);
2859 assert_eq!(after_count.num_tx_files, 1);
2860 }
2861
2862 #[tokio::test]
2863 async fn dont_cleanup_in_progress_write() {
2864 let mut fixture = MockDatasetFixture::try_new().unwrap();
2871 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2872 fixture.create_some_data().await.unwrap();
2873 fixture.block_commits();
2874 assert!(fixture.append_some_data().await.is_err());
2875
2876 let before_count = fixture.count_files().await.unwrap();
2877
2878 let removed = fixture
2879 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2880 .await
2881 .unwrap();
2882
2883 assert_eq!(removed.old_versions, 0);
2884 assert_eq!(removed.bytes_removed, 0);
2885 assert_eq!(removed.data_files_removed, 0);
2886
2887 let after_count = fixture.count_files().await.unwrap();
2888 assert_eq!(before_count, after_count);
2889 }
2890
2891 #[tokio::test]
2892 async fn can_recover_delete_failure() {
2893 let mut fixture = MockDatasetFixture::try_new().unwrap();
2896 fixture.create_some_data().await.unwrap();
2897 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
2898 fixture.overwrite_some_data().await.unwrap();
2899
2900 fixture.block_delete_manifest();
2904
2905 let before_count = fixture.count_files().await.unwrap();
2906 assert_eq!(before_count.num_data_files, 2);
2907 assert_eq!(before_count.num_manifest_files, 2);
2908
2909 assert!(
2910 fixture
2911 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2912 .await
2913 .is_err()
2914 );
2915
2916 let mid_count = fixture.count_files().await.unwrap();
2922 assert_eq!(mid_count.num_data_files, 1);
2923 assert_eq!(mid_count.num_manifest_files, 2);
2924
2925 fixture.unblock_delete_manifest();
2926
2927 let removed = fixture
2928 .run_cleanup(utc_now() - TimeDelta::try_days(7).unwrap())
2929 .await
2930 .unwrap();
2931
2932 let after_count = fixture.count_files().await.unwrap();
2933 assert_eq!(removed.old_versions, 1);
2934 assert_eq!(
2935 removed.bytes_removed,
2936 mid_count.num_bytes - after_count.num_bytes
2937 );
2938
2939 assert_eq!(after_count.num_data_files, 1);
2940 assert_eq!(after_count.num_manifest_files, 1);
2941 }
2942
2943 #[tokio::test]
2944 async fn cleanup_and_retain_3_recent_versions() {
2945 let fixture = MockDatasetFixture::try_new().unwrap();
2946 fixture.create_some_data().await.unwrap();
2947 let mut time = 10i64;
2948 for _ in 0..4 {
2949 MockClock::set_system_time(TimeDelta::try_seconds(time).unwrap().to_std().unwrap());
2950 time += 10i64;
2951 fixture.overwrite_some_data().await.unwrap();
2952 }
2953
2954 let before_count = fixture.count_files().await.unwrap();
2955 assert_eq!(before_count.num_data_files, 5);
2956 assert_eq!(before_count.num_manifest_files, 5);
2957
2958 let policy = CleanupPolicyBuilder::default()
2960 .retain_n_versions(&fixture.open().await.unwrap(), 3)
2961 .await
2962 .unwrap()
2963 .build();
2964 let removed = fixture.run_cleanup_with_policy(policy).await.unwrap();
2965
2966 let after_count = fixture.count_files().await.unwrap();
2967 assert_eq!(removed.old_versions, 2);
2968 assert_eq!(
2969 removed.bytes_removed,
2970 before_count.num_bytes - after_count.num_bytes
2971 );
2972
2973 assert_eq!(after_count.num_data_files, 3);
2974 assert_eq!(after_count.num_manifest_files, 3);
2975 }
2976
2977 #[tokio::test]
2978 async fn cleanup_before_ts_and_retain_n_recent_versions() {
2979 let fixture = MockDatasetFixture::try_new().unwrap();
2980 fixture.create_some_data().await.unwrap();
2981 for time in (1i64..).take(4) {
2982 MockClock::set_system_time(TimeDelta::try_days(time).unwrap().to_std().unwrap());
2983 fixture.overwrite_some_data().await.unwrap();
2984 }
2985
2986 let before_count = fixture.count_files().await.unwrap();
2987 assert_eq!(before_count.num_data_files, 5);
2988 assert_eq!(before_count.num_manifest_files, 5);
2989
2990 let policy = CleanupPolicyBuilder::default()
2992 .before_timestamp(utc_now() - TimeDelta::try_days(6).unwrap())
2993 .retain_n_versions(&fixture.open().await.unwrap(), 3)
2994 .await
2995 .unwrap()
2996 .build();
2997 let removed = fixture.run_cleanup_with_policy(policy).await.unwrap();
2998 assert_eq!(removed.old_versions, 0);
2999
3000 let policy = CleanupPolicyBuilder::default()
3002 .before_timestamp(utc_now())
3003 .retain_n_versions(&fixture.open().await.unwrap(), 10)
3004 .await
3005 .unwrap()
3006 .build();
3007 let removed = fixture.run_cleanup_with_policy(policy).await.unwrap();
3008 assert_eq!(removed.old_versions, 0);
3009
3010 let policy = CleanupPolicyBuilder::default()
3012 .before_timestamp(utc_now() - TimeDelta::try_days(2).unwrap())
3013 .retain_n_versions(&fixture.open().await.unwrap(), 3)
3014 .await
3015 .unwrap()
3016 .build();
3017 let removed = fixture.run_cleanup_with_policy(policy).await.unwrap();
3018
3019 let after_count = fixture.count_files().await.unwrap();
3020 assert_eq!(removed.old_versions, 2);
3021 assert_eq!(
3022 removed.bytes_removed,
3023 before_count.num_bytes - after_count.num_bytes
3024 );
3025 assert_eq!(after_count.num_data_files, 3);
3026 assert_eq!(after_count.num_manifest_files, 3);
3027 }
3028
3029 #[tokio::test]
3030 async fn cleanup_preserves_unmanaged_dirs_and_files() {
3031 let fixture = MockDatasetFixture::try_new().unwrap();
3034 fixture.create_some_data().await.unwrap();
3035
3036 let registry = Arc::new(ObjectStoreRegistry::default());
3037 let (os, base) =
3038 ObjectStore::from_uri_and_params(registry, &fixture.dataset_path, &fixture.os_params())
3039 .await
3040 .unwrap();
3041
3042 let img = base.clone().join("images").join("clip.mp4");
3044 let misc = base.clone().join("misc").join("notes.txt");
3045 let branch_file = base.clone().join("tree").join("branchA").join("data.bin");
3046 os.put(&img, b"video").await.unwrap();
3047 os.put(&misc, b"notes").await.unwrap();
3048 os.put(&branch_file, b"branch").await.unwrap();
3049
3050 let tmp_manifest = base.clone().join("_versions").join(".tmp").join("orphan");
3052 os.put(&tmp_manifest, b"tmp").await.unwrap();
3053 os.remove_dir_all(base.clone().join(TRANSACTIONS_DIR))
3055 .await
3056 .unwrap();
3057
3058 fixture
3059 .run_cleanup_with_override(utc_now(), Some(true), Some(false))
3060 .await
3061 .unwrap();
3062
3063 assert!(!os.exists(&tmp_manifest).await.unwrap());
3065 assert!(os.exists(&img).await.unwrap());
3067 assert!(os.exists(&misc).await.unwrap());
3068 assert!(os.exists(&branch_file).await.unwrap());
3069 }
3070
3071 struct LineageSetup {
3096 main: BranchDatasetFixture,
3097 branch1: BranchDatasetFixture,
3098 branch2: BranchDatasetFixture,
3099 branch3: BranchDatasetFixture,
3100 branch4: BranchDatasetFixture,
3101 }
3102
3103 impl LineageSetup {
3104 pub async fn assert_all_unchanged(&mut self) {
3106 self.main.assert_not_changed().await.unwrap();
3107 self.branch1.assert_not_changed().await.unwrap();
3108 self.branch2.assert_not_changed().await.unwrap();
3109 self.branch3.assert_not_changed().await.unwrap();
3110 self.branch4.assert_not_changed().await.unwrap();
3111 }
3112
3113 pub async fn assert_unchanged(&mut self, branches: &[&str]) {
3115 for &b in branches {
3116 match b {
3117 "main" => self.main.assert_not_changed().await.unwrap(),
3118 "branch1" => self.branch1.assert_not_changed().await.unwrap(),
3119 "branch2" => self.branch2.assert_not_changed().await.unwrap(),
3120 "branch3" => self.branch3.assert_not_changed().await.unwrap(),
3121 "branch4" => self.branch4.assert_not_changed().await.unwrap(),
3122 _ => panic!("unknown branch: {}", b),
3123 }
3124 }
3125 }
3126
3127 pub async fn enable_auto_cleanup(&mut self) -> Result<()> {
3128 let updates = [
3129 ("lance.auto_cleanup.interval", "1"),
3130 ("lance.auto_cleanup.retain_versions", "1"),
3131 ("lance.auto_cleanup.referenced_branch", "true"),
3132 ];
3133 self.main.dataset.update_config(updates).await?;
3134 self.branch1.dataset.update_config(updates).await?;
3135 self.branch2.dataset.update_config(updates).await?;
3136 self.branch3.dataset.update_config(updates).await?;
3137 self.branch4.dataset.update_config(updates).await?;
3138 self.main.refresh().await?;
3139 self.branch1.refresh().await?;
3140 self.branch2.refresh().await?;
3141 self.branch3.refresh().await?;
3142 self.branch4.refresh().await?;
3143 Ok(())
3144 }
3145
3146 pub async fn disable_auto_cleanup(&mut self) -> Result<()> {
3147 let updates = [
3148 ("lance.auto_cleanup.interval", None),
3149 ("lance.auto_cleanup.retain_versions", None),
3150 ("lance.auto_cleanup.older_than", None),
3151 ];
3152 self.main.dataset.update_config(updates).await?;
3153 self.branch1.dataset.update_config(updates).await?;
3154 self.branch2.dataset.update_config(updates).await?;
3155 self.branch3.dataset.update_config(updates).await?;
3156 self.branch4.dataset.update_config(updates).await?;
3157 self.main.refresh().await?;
3158 self.branch1.refresh().await?;
3159 self.branch2.refresh().await?;
3160 self.branch3.refresh().await?;
3161 self.branch4.refresh().await?;
3162 Ok(())
3163 }
3164 }
3165
3166 async fn build_lineage_datasets() -> Result<LineageSetup> {
3168 let fixture = Arc::new(MockDatasetFixture::try_new()?);
3169
3170 MockClock::set_system_time(TimeDelta::try_seconds(1).unwrap().to_std().unwrap());
3171
3172 use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, StringArray};
3174 use arrow_schema::{DataType, Field};
3175 let ids = Int32Array::from_iter_values(0..50i32);
3176 let texts = StringArray::from_iter_values((0..50i32).map(|i| format!("text_{}", i)));
3177 let schema = Arc::new(arrow_schema::Schema::new(vec![
3178 Field::new("id", DataType::Int32, false),
3179 Field::new("text", DataType::Utf8, false),
3180 ]));
3181 let batch =
3182 RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(texts)]).unwrap();
3183 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema);
3184
3185 Dataset::write(
3186 reader,
3187 &fixture.dataset_path,
3188 Some(WriteParams {
3189 mode: WriteMode::Create,
3190 store_params: Some(fixture.os_params()),
3191 ..Default::default()
3192 }),
3193 )
3194 .await?;
3195 let mut main = BranchDatasetFixture::new(fixture.clone(), fixture.load().await?);
3196 main.create_text_index().await?;
3198 main.write_data().await?;
3199
3200 let mut branch1 = BranchDatasetFixture::new(
3202 fixture.clone(),
3203 fixture
3204 .create_branch_and_load(&mut main.dataset, "branch1", (None, None))
3205 .await?,
3206 );
3207 branch1.write_data().await?;
3208
3209 let mut branch2 = BranchDatasetFixture::new(
3211 fixture.clone(),
3212 fixture
3213 .create_branch_and_load(&mut branch1.dataset, "dev/branch2", ("branch1", None))
3214 .await?,
3215 );
3216 branch2.write_data().await?;
3217
3218 let mut branch3 = BranchDatasetFixture::new(
3220 fixture.clone(),
3221 fixture
3222 .create_branch_and_load(
3223 &mut branch2.dataset,
3224 "feature/nathan/branch3",
3225 ("dev/branch2", None),
3226 )
3227 .await?,
3228 );
3229 branch3.write_data().await?;
3230
3231 main.write_data().await?;
3233 let mut branch4 = BranchDatasetFixture::new(
3234 fixture.clone(),
3235 fixture
3236 .create_branch_and_load(&mut main.dataset, "branch4", (None, None))
3237 .await?,
3238 );
3239 branch4.write_data().await?;
3240
3241 let mut lineage = LineageSetup {
3242 main,
3243 branch1,
3244 branch2,
3245 branch3,
3246 branch4,
3247 };
3248
3249 lineage.disable_auto_cleanup().await?;
3250 Ok(lineage)
3251 }
3252
3253 struct BranchDatasetFixture {
3259 fixture: Arc<MockDatasetFixture>,
3260 dataset: Dataset,
3261 counts: FileCounts,
3262 }
3263
3264 impl BranchDatasetFixture {
3265 fn new(fixture: Arc<MockDatasetFixture>, dataset: Dataset) -> Self {
3266 Self {
3267 fixture,
3268 dataset,
3269 counts: FileCounts {
3270 num_manifest_files: 0,
3271 num_data_files: 0,
3272 num_tx_files: 0,
3273 num_delete_files: 0,
3274 num_index_files: 0,
3275 num_bytes: 0,
3276 },
3277 }
3278 }
3279
3280 async fn create_text_index(&mut self) -> Result<()> {
3283 use crate::index::DatasetIndexExt;
3284 use lance_index::IndexType;
3285 use lance_index::scalar::InvertedIndexParams;
3286 let params = InvertedIndexParams::default();
3287 self.dataset
3288 .create_index(&["text"], IndexType::Inverted, None, ¶ms, true)
3289 .await?;
3290 Ok(())
3291 }
3292
3293 async fn append_delete_and_optimize_index(&mut self) -> Result<()> {
3295 self.write_batch(5).await?;
3297 self.delete_last_row().await?;
3299 use lance_index::optimize::OptimizeOptions;
3301 self.dataset
3302 .optimize_indices(&OptimizeOptions::merge(1))
3303 .await?;
3304 Ok(())
3305 }
3306
3307 async fn write_batch(&mut self, rows: i32) -> Result<()> {
3309 use crate::dataset::WriteParams;
3310 use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, StringArray};
3311 use arrow_schema::{DataType, Field};
3312
3313 let ids = Int32Array::from_iter_values(0..rows);
3314 let texts = StringArray::from_iter_values((0..rows).map(|i| format!("text_{}", i)));
3315 let schema = Arc::new(arrow_schema::Schema::new(vec![
3316 Field::new("id", DataType::Int32, false),
3317 Field::new("text", DataType::Utf8, false),
3318 ]));
3319 let batch =
3320 RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(texts)]).unwrap();
3321 let reader = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema);
3322
3323 self.dataset
3324 .append(
3325 reader,
3326 Some(WriteParams {
3327 mode: WriteMode::Append,
3328 store_params: Some(self.fixture.os_params()),
3329 ..Default::default()
3330 }),
3331 )
3332 .await?;
3333 self.dataset.checkout_latest().await?;
3334 Ok(())
3335 }
3336
3337 async fn delete_last_row(&mut self) -> Result<()> {
3339 let batch = self.dataset.scan().with_row_id().try_into_batch().await?;
3340 if batch.num_rows() > 0 {
3341 let row_id_col = batch.column_by_name(lance_core::ROW_ID).unwrap();
3342 let uint64_array = row_id_col.as_any().downcast_ref::<UInt64Array>().unwrap();
3343 let max_row_id = compute::max(uint64_array).unwrap_or(0);
3344 self.dataset
3345 .delete(&format!("_rowid = {}", max_row_id))
3346 .await?;
3347 }
3348 Ok(())
3349 }
3350
3351 async fn refresh(&mut self) -> Result<()> {
3353 use futures::TryStreamExt;
3354 let branch_path = self.dataset.base.clone();
3355
3356 async fn count_dir(
3358 os: &ObjectStore,
3359 dir: &Path,
3360 exts: Option<&[&str]>,
3361 ) -> Result<usize> {
3362 let mut count = 0usize;
3363 let mut s = os.read_dir_all(dir, None);
3364 while let Some(meta) = s.try_next().await? {
3365 match exts {
3366 Some(exts) => {
3367 if let Some(e) = meta.location.extension()
3368 && exts.contains(&e)
3369 {
3370 count += 1;
3371 }
3372 }
3373 None => count += 1,
3374 }
3375 }
3376 Ok(count)
3377 }
3378
3379 let manifest_dir = branch_path.clone().join("_versions");
3380 self.counts.num_manifest_files = count_dir(
3381 &self.dataset.object_store,
3382 &manifest_dir,
3383 Some(&["manifest"]),
3384 )
3385 .await
3386 .unwrap_or(0);
3387
3388 let txn_dir = branch_path.clone().join("_transactions");
3390 self.counts.num_tx_files =
3391 count_dir(&self.dataset.object_store, &txn_dir, Some(&["txn"]))
3392 .await
3393 .unwrap_or(0);
3394
3395 let idx_dir = branch_path.clone().join(crate::dataset::INDICES_DIR);
3397 self.counts.num_index_files = count_dir(&self.dataset.object_store, &idx_dir, None)
3398 .await
3399 .unwrap_or(0);
3400
3401 let del_dir = branch_path.clone().join("_deletions");
3403 self.counts.num_delete_files = count_dir(
3404 &self.dataset.object_store,
3405 &del_dir,
3406 Some(&["arrow", "bin"]),
3407 )
3408 .await
3409 .unwrap_or(0);
3410
3411 let data_dir = branch_path.clone().join(crate::dataset::DATA_DIR);
3413 self.counts.num_data_files =
3414 count_dir(&self.dataset.object_store, &data_dir, Some(&["lance"]))
3415 .await
3416 .unwrap_or(0);
3417
3418 Ok(())
3419 }
3420
3421 async fn count_data(&self) -> Result<usize> {
3422 use futures::TryStreamExt;
3423 let mut count = 0usize;
3424 let mut s = self.dataset.scan().try_into_stream().await?;
3425 while let Some(_batch) = s.try_next().await? {
3426 count += 1;
3427 }
3428 Ok(count)
3429 }
3430
3431 async fn assert_not_changed(&mut self) -> Result<()> {
3433 let pre_counts = self.counts;
3434 let pre_data_count = self.count_data().await?;
3435
3436 self.refresh().await?;
3437 assert_eq!(
3438 self.counts.num_manifest_files,
3439 pre_counts.num_manifest_files
3440 );
3441 assert_eq!(self.counts.num_data_files, pre_counts.num_data_files);
3442 assert_eq!(self.counts.num_tx_files, pre_counts.num_tx_files);
3443 assert_eq!(self.counts.num_delete_files, pre_counts.num_delete_files);
3444 assert_eq!(self.counts.num_index_files, pre_counts.num_index_files);
3445 assert_eq!(self.count_data().await?, pre_data_count);
3446 Ok(())
3447 }
3448
3449 async fn write_data(&mut self) -> Result<()> {
3451 self.append_delete_and_optimize_index().await?;
3452 self.refresh().await
3453 }
3454
3455 async fn compact(&mut self) -> Result<()> {
3457 use crate::dataset::optimize::{CompactionOptions, compact_files};
3458 compact_files(&mut self.dataset, CompactionOptions::default(), None).await?;
3459 self.refresh().await
3460 }
3461
3462 async fn run_cleanup(&mut self) -> Result<RemovalStats> {
3463 let policy = CleanupPolicyBuilder::default()
3464 .error_if_tagged_old_versions(false)
3465 .retain_n_versions(&self.dataset, 1)
3466 .await?
3467 .build();
3468 self.run_cleanup_inner(policy).await
3469 }
3470
3471 async fn run_cleanup_with_referenced_branches(&mut self) -> Result<RemovalStats> {
3472 let policy = CleanupPolicyBuilder::default()
3473 .error_if_tagged_old_versions(false)
3474 .clean_referenced_branches(true)
3475 .retain_n_versions(&self.dataset, 1)
3476 .await?
3477 .build();
3478 self.run_cleanup_inner(policy).await
3479 }
3480
3481 async fn explain_cleanup_with_referenced_branches(&mut self) -> Result<CleanupExplanation> {
3482 let policy = CleanupPolicyBuilder::default()
3483 .error_if_tagged_old_versions(false)
3484 .clean_referenced_branches(true)
3485 .retain_n_versions(&self.dataset, 1)
3486 .await?
3487 .build();
3488 self.dataset.checkout_latest().await?;
3489 self.dataset.cleanup(policy).explain().await
3490 }
3491
3492 async fn run_cleanup_inner(&mut self, policy: CleanupPolicy) -> Result<RemovalStats> {
3493 let pre_count = self.count_data().await?;
3494 self.dataset.checkout_latest().await?;
3495 let stats = cleanup_old_versions(&self.dataset, policy).await;
3496 self.refresh().await?;
3497 assert_eq!(self.count_data().await?, pre_count);
3499 stats
3500 }
3501 }
3502
3503 #[tokio::test]
3505 async fn cleanup_lineage_branch1() {
3506 let mut setup = build_lineage_datasets().await.unwrap();
3507
3508 setup.branch1.write_data().await.unwrap();
3509 setup.branch1.run_cleanup().await.unwrap();
3510 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
3517 assert_eq!(setup.branch1.counts.num_data_files, 2);
3518 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3519 assert_eq!(setup.branch1.counts.num_delete_files, 2);
3520 assert_eq!(setup.branch1.counts.num_index_files, 14);
3521 setup.assert_all_unchanged().await;
3522
3523 setup.branch1.compact().await.unwrap();
3524 setup.branch1.run_cleanup().await.unwrap();
3525 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
3532 assert_eq!(setup.branch1.counts.num_data_files, 2);
3533 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3534 assert_eq!(setup.branch1.counts.num_delete_files, 1);
3535 assert_eq!(setup.branch1.counts.num_index_files, 14);
3536 setup.assert_all_unchanged().await;
3537
3538 setup.branch2.compact().await.unwrap();
3540 setup.branch3.compact().await.unwrap();
3541 setup.branch3.run_cleanup().await.unwrap();
3542 setup.branch2.run_cleanup().await.unwrap();
3543 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
3546 assert_eq!(setup.branch2.counts.num_data_files, 1);
3547 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3548 assert_eq!(setup.branch2.counts.num_delete_files, 0);
3549 assert_eq!(setup.branch2.counts.num_index_files, 7);
3550 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3553 assert_eq!(setup.branch3.counts.num_data_files, 1);
3554 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3555 assert_eq!(setup.branch3.counts.num_delete_files, 0);
3556 assert_eq!(setup.branch3.counts.num_index_files, 7);
3557 setup.branch1.run_cleanup().await.unwrap();
3558
3559 assert_eq!(setup.branch1.counts.num_manifest_files, 1);
3562 assert_eq!(setup.branch1.counts.num_data_files, 1);
3563 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3564 assert_eq!(setup.branch1.counts.num_delete_files, 0);
3565 assert_eq!(setup.branch1.counts.num_index_files, 7);
3566 setup.assert_all_unchanged().await;
3567 }
3568
3569 #[tokio::test]
3570 async fn cleanup_lineage_branch3() {
3571 let mut setup = build_lineage_datasets().await.unwrap();
3572
3573 setup.branch3.write_data().await.unwrap();
3574 setup.branch3.run_cleanup().await.unwrap();
3575 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3579 assert_eq!(setup.branch3.counts.num_data_files, 2);
3580 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3581 assert_eq!(setup.branch3.counts.num_delete_files, 2);
3582 assert_eq!(setup.branch3.counts.num_index_files, 7);
3583 setup
3584 .assert_unchanged(&["branch1", "branch2", "branch4", "main"])
3585 .await;
3586
3587 setup.branch2.compact().await.unwrap();
3588 setup.branch2.run_cleanup().await.unwrap();
3589 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
3595 assert_eq!(setup.branch2.counts.num_data_files, 2);
3596 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3597 assert_eq!(setup.branch2.counts.num_delete_files, 1);
3598 assert_eq!(setup.branch2.counts.num_index_files, 7);
3599
3600 setup.branch3.compact().await.unwrap();
3601 setup.branch3.run_cleanup().await.unwrap();
3602 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3605 assert_eq!(setup.branch3.counts.num_data_files, 1);
3606 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3607 assert_eq!(setup.branch3.counts.num_delete_files, 0);
3608 assert_eq!(setup.branch3.counts.num_index_files, 7);
3609 setup
3610 .assert_unchanged(&["branch1", "branch2", "branch4", "main"])
3611 .await;
3612
3613 setup.branch2.compact().await.unwrap();
3614 setup.branch2.run_cleanup().await.unwrap();
3615 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
3618 assert_eq!(setup.branch2.counts.num_data_files, 1);
3619 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3620 assert_eq!(setup.branch2.counts.num_delete_files, 0);
3621 assert_eq!(setup.branch2.counts.num_index_files, 7);
3622 }
3623
3624 #[tokio::test]
3625 async fn cleanup_lineage_branch4() {
3626 let mut setup = build_lineage_datasets().await.unwrap();
3628
3629 setup.branch4.write_data().await.unwrap();
3630 setup.branch4.run_cleanup().await.unwrap();
3631 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
3635 assert_eq!(setup.branch4.counts.num_data_files, 2);
3636 assert_eq!(setup.branch4.counts.num_tx_files, 1);
3637 assert_eq!(setup.branch4.counts.num_delete_files, 2);
3638 assert_eq!(setup.branch4.counts.num_index_files, 7);
3639 setup.assert_all_unchanged().await;
3640
3641 setup.main.compact().await.unwrap();
3642 setup.main.run_cleanup().await.unwrap();
3643 assert_eq!(setup.main.counts.num_manifest_files, 3);
3655 assert_eq!(setup.main.counts.num_data_files, 4);
3656 assert_eq!(setup.main.counts.num_tx_files, 1);
3657 assert_eq!(setup.main.counts.num_delete_files, 2);
3658 assert_eq!(setup.main.counts.num_index_files, 14);
3659
3660 setup.branch4.compact().await.unwrap();
3661 setup.branch4.run_cleanup().await.unwrap();
3662 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
3665 assert_eq!(setup.branch4.counts.num_data_files, 1);
3666 assert_eq!(setup.branch4.counts.num_tx_files, 1);
3667 assert_eq!(setup.branch4.counts.num_delete_files, 0);
3668 assert_eq!(setup.branch4.counts.num_index_files, 7);
3669 setup.assert_all_unchanged().await;
3670
3671 setup.main.run_cleanup().await.unwrap();
3672 assert_eq!(setup.main.counts.num_manifest_files, 2);
3679 assert_eq!(setup.main.counts.num_data_files, 3);
3680 assert_eq!(setup.main.counts.num_tx_files, 1);
3681 assert_eq!(setup.main.counts.num_delete_files, 1);
3682 assert_eq!(setup.main.counts.num_index_files, 14);
3683 }
3684
3685 #[tokio::test]
3686 async fn cleanup_lineage_main() {
3687 let mut setup = build_lineage_datasets().await.unwrap();
3689
3690 setup.main.write_data().await.unwrap();
3691 setup.main.run_cleanup().await.unwrap();
3692 assert_eq!(setup.main.counts.num_manifest_files, 3);
3704 assert_eq!(setup.main.counts.num_data_files, 4);
3705 assert_eq!(setup.main.counts.num_tx_files, 1);
3706 assert_eq!(setup.main.counts.num_delete_files, 3);
3707 assert_eq!(setup.main.counts.num_index_files, 21);
3708 setup.assert_all_unchanged().await;
3709
3710 setup.main.compact().await.unwrap();
3711 setup.main.run_cleanup().await.unwrap();
3712 assert_eq!(setup.main.counts.num_manifest_files, 3);
3715 assert_eq!(setup.main.counts.num_data_files, 4);
3716 assert_eq!(setup.main.counts.num_tx_files, 1);
3717 assert_eq!(setup.main.counts.num_delete_files, 2);
3718 assert_eq!(setup.main.counts.num_index_files, 21);
3719 setup.assert_all_unchanged().await;
3720
3721 setup.branch1.write_data().await.unwrap();
3722 setup.branch1.compact().await.unwrap();
3723 setup.branch2.write_data().await.unwrap();
3724 setup.branch2.compact().await.unwrap();
3725 setup.branch2.run_cleanup().await.unwrap();
3726 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
3736 assert_eq!(setup.branch2.counts.num_data_files, 2);
3737 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3738 assert_eq!(setup.branch2.counts.num_delete_files, 1);
3739 assert_eq!(setup.branch2.counts.num_index_files, 14);
3740 setup.branch1.run_cleanup().await.unwrap();
3741 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
3743 assert_eq!(setup.branch1.counts.num_data_files, 2);
3744 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3745 assert_eq!(setup.branch1.counts.num_delete_files, 1);
3746 assert_eq!(setup.branch1.counts.num_index_files, 7);
3747
3748 setup.main.run_cleanup().await.unwrap();
3749 assert_eq!(setup.main.counts.num_manifest_files, 3);
3759 assert_eq!(setup.main.counts.num_data_files, 4);
3760 assert_eq!(setup.main.counts.num_tx_files, 1);
3761 assert_eq!(setup.main.counts.num_delete_files, 2);
3762 assert_eq!(setup.main.counts.num_index_files, 14);
3763
3764 setup.branch3.write_data().await.unwrap();
3765 setup.branch3.compact().await.unwrap();
3766 setup.branch3.run_cleanup().await.unwrap();
3767 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3769 assert_eq!(setup.branch3.counts.num_data_files, 1);
3770 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3771 assert_eq!(setup.branch3.counts.num_delete_files, 0);
3772 assert_eq!(setup.branch3.counts.num_index_files, 7);
3773
3774 setup.main.run_cleanup().await.unwrap();
3775 assert_eq!(setup.main.counts.num_manifest_files, 3);
3777 assert_eq!(setup.main.counts.num_data_files, 4);
3778 assert_eq!(setup.main.counts.num_tx_files, 1);
3779 assert_eq!(setup.main.counts.num_delete_files, 2);
3780 assert_eq!(setup.main.counts.num_index_files, 14);
3781
3782 setup.branch1.run_cleanup().await.unwrap();
3784 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
3785 assert_eq!(setup.branch1.counts.num_data_files, 2);
3786 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3787 assert_eq!(setup.branch1.counts.num_delete_files, 1);
3788 assert_eq!(setup.branch1.counts.num_index_files, 7);
3789
3790 setup.branch2.run_cleanup().await.unwrap();
3791 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
3794 assert_eq!(setup.branch2.counts.num_data_files, 1);
3795 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3796 assert_eq!(setup.branch2.counts.num_delete_files, 0);
3797 assert_eq!(setup.branch2.counts.num_index_files, 7);
3798
3799 setup.branch1.run_cleanup().await.unwrap();
3800 assert_eq!(setup.branch1.counts.num_manifest_files, 1);
3803 assert_eq!(setup.branch1.counts.num_data_files, 1);
3804 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3805 assert_eq!(setup.branch1.counts.num_delete_files, 0);
3806 assert_eq!(setup.branch1.counts.num_index_files, 7);
3807
3808 setup.main.run_cleanup().await.unwrap();
3809 assert_eq!(setup.main.counts.num_manifest_files, 2);
3815 assert_eq!(setup.main.counts.num_data_files, 4);
3816 assert_eq!(setup.main.counts.num_tx_files, 1);
3817 assert_eq!(setup.main.counts.num_delete_files, 2);
3818 assert_eq!(setup.main.counts.num_index_files, 14);
3819
3820 setup.branch4.write_data().await.unwrap();
3821 setup.branch4.compact().await.unwrap();
3822 setup.branch4.run_cleanup().await.unwrap();
3823 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
3826 assert_eq!(setup.branch4.counts.num_data_files, 1);
3827 assert_eq!(setup.branch4.counts.num_tx_files, 1);
3828 assert_eq!(setup.branch4.counts.num_delete_files, 0);
3829 assert_eq!(setup.branch4.counts.num_index_files, 7);
3830
3831 setup.main.run_cleanup().await.unwrap();
3832 assert_eq!(setup.main.counts.num_manifest_files, 1);
3835 assert_eq!(setup.main.counts.num_data_files, 1);
3836 assert_eq!(setup.main.counts.num_tx_files, 1);
3837 assert_eq!(setup.main.counts.num_delete_files, 0);
3838 assert_eq!(setup.main.counts.num_index_files, 7);
3839 }
3840
3841 #[tokio::test]
3842 async fn auto_clean_referenced_branches_from_branch2() {
3843 let mut setup = build_lineage_datasets().await.unwrap();
3845
3846 setup.branch3.write_data().await.unwrap();
3847 setup.enable_auto_cleanup().await.unwrap();
3848 setup
3849 .branch2
3850 .run_cleanup_with_referenced_branches()
3851 .await
3852 .unwrap();
3853 setup.branch3.refresh().await.unwrap();
3854 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
3859 assert_eq!(setup.branch2.counts.num_data_files, 1);
3860 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3861 assert_eq!(setup.branch2.counts.num_delete_files, 1);
3862 assert_eq!(setup.branch2.counts.num_index_files, 7);
3863 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3867 assert_eq!(setup.branch3.counts.num_data_files, 2);
3868 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3869 assert_eq!(setup.branch3.counts.num_delete_files, 2);
3870 assert_eq!(setup.branch3.counts.num_index_files, 7);
3871 setup
3872 .assert_unchanged(&["branch1", "branch4", "main"])
3873 .await;
3874
3875 setup.disable_auto_cleanup().await.unwrap();
3876 setup.branch2.write_data().await.unwrap();
3877 setup.branch2.compact().await.unwrap();
3878 setup.branch3.compact().await.unwrap();
3879 setup.enable_auto_cleanup().await.unwrap();
3880 setup
3881 .branch2
3882 .run_cleanup_with_referenced_branches()
3883 .await
3884 .unwrap();
3885 setup.branch3.refresh().await.unwrap();
3886 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
3889 assert_eq!(setup.branch2.counts.num_data_files, 1);
3890 assert_eq!(setup.branch2.counts.num_tx_files, 1);
3891 assert_eq!(setup.branch2.counts.num_delete_files, 0);
3892 assert_eq!(setup.branch2.counts.num_index_files, 7);
3893 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
3896 assert_eq!(setup.branch3.counts.num_data_files, 1);
3897 assert_eq!(setup.branch3.counts.num_tx_files, 1);
3898 assert_eq!(setup.branch3.counts.num_delete_files, 0);
3899 assert_eq!(setup.branch3.counts.num_index_files, 7);
3900 setup
3901 .assert_unchanged(&["branch1", "branch4", "main"])
3902 .await;
3903 }
3904
3905 #[tokio::test]
3906 async fn auto_clean_referenced_branches_from_main() {
3907 let mut setup = build_lineage_datasets().await.unwrap();
3908
3909 setup.enable_auto_cleanup().await.unwrap();
3910 setup.main.write_data().await.unwrap();
3911 setup
3912 .main
3913 .run_cleanup_with_referenced_branches()
3914 .await
3915 .unwrap();
3916 assert_eq!(setup.main.counts.num_manifest_files, 3);
3926 assert_eq!(setup.main.counts.num_data_files, 4);
3927 assert_eq!(setup.main.counts.num_tx_files, 1);
3928 assert_eq!(setup.main.counts.num_delete_files, 3);
3929 assert_eq!(setup.main.counts.num_index_files, 7);
3930
3931 setup.main.compact().await.unwrap();
3932 setup
3933 .main
3934 .run_cleanup_with_referenced_branches()
3935 .await
3936 .unwrap();
3937 assert_eq!(setup.main.counts.num_manifest_files, 3);
3946 assert_eq!(setup.main.counts.num_data_files, 4);
3947 assert_eq!(setup.main.counts.num_tx_files, 1);
3948 assert_eq!(setup.main.counts.num_delete_files, 2);
3949 assert_eq!(setup.main.counts.num_index_files, 7);
3950
3951 setup.branch4.compact().await.unwrap();
3952 setup
3953 .main
3954 .run_cleanup_with_referenced_branches()
3955 .await
3956 .unwrap();
3957 setup.branch4.refresh().await.unwrap();
3958 assert_eq!(setup.main.counts.num_manifest_files, 2);
3963 assert_eq!(setup.main.counts.num_data_files, 3);
3964 assert_eq!(setup.main.counts.num_tx_files, 1);
3965 assert_eq!(setup.main.counts.num_delete_files, 1);
3966 assert_eq!(setup.main.counts.num_index_files, 7);
3967 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
3969 assert_eq!(setup.branch4.counts.num_data_files, 1);
3970 assert_eq!(setup.branch4.counts.num_tx_files, 1);
3971 assert_eq!(setup.branch4.counts.num_delete_files, 0);
3972 assert_eq!(setup.branch4.counts.num_index_files, 7);
3973
3974 setup.branch1.write_data().await.unwrap();
3975 setup.branch1.compact().await.unwrap();
3976 setup
3977 .main
3978 .run_cleanup_with_referenced_branches()
3979 .await
3980 .unwrap();
3981 setup.branch1.refresh().await.unwrap();
3982 assert_eq!(setup.main.counts.num_manifest_files, 2);
3987 assert_eq!(setup.main.counts.num_data_files, 3);
3988 assert_eq!(setup.main.counts.num_tx_files, 1);
3989 assert_eq!(setup.main.counts.num_delete_files, 1);
3990 assert_eq!(setup.main.counts.num_index_files, 7);
3991 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
3996 assert_eq!(setup.branch1.counts.num_data_files, 2);
3997 assert_eq!(setup.branch1.counts.num_tx_files, 1);
3998 assert_eq!(setup.branch1.counts.num_delete_files, 1);
3999 assert_eq!(setup.branch1.counts.num_index_files, 7);
4000
4001 setup.branch2.write_data().await.unwrap();
4002 setup.branch2.compact().await.unwrap();
4003 setup
4004 .main
4005 .run_cleanup_with_referenced_branches()
4006 .await
4007 .unwrap();
4008 setup.branch2.refresh().await.unwrap();
4009 assert_eq!(setup.main.counts.num_manifest_files, 2);
4014 assert_eq!(setup.main.counts.num_data_files, 3);
4015 assert_eq!(setup.main.counts.num_tx_files, 1);
4016 assert_eq!(setup.main.counts.num_delete_files, 1);
4017 assert_eq!(setup.main.counts.num_index_files, 7);
4018 assert_eq!(setup.branch1.counts.num_manifest_files, 2);
4023 assert_eq!(setup.branch1.counts.num_data_files, 2);
4024 assert_eq!(setup.branch1.counts.num_tx_files, 1);
4025 assert_eq!(setup.branch1.counts.num_delete_files, 1);
4026 assert_eq!(setup.branch1.counts.num_index_files, 7);
4027 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
4032 assert_eq!(setup.branch2.counts.num_data_files, 2);
4033 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4034 assert_eq!(setup.branch2.counts.num_delete_files, 1);
4035 assert_eq!(setup.branch2.counts.num_index_files, 7);
4036
4037 setup.branch3.write_data().await.unwrap();
4038 setup.branch3.compact().await.unwrap();
4039 setup
4040 .main
4041 .run_cleanup_with_referenced_branches()
4042 .await
4043 .unwrap();
4044 setup.branch1.refresh().await.unwrap();
4045 setup.branch2.refresh().await.unwrap();
4046 setup.branch3.refresh().await.unwrap();
4047 assert_eq!(setup.main.counts.num_manifest_files, 1);
4050 assert_eq!(setup.main.counts.num_data_files, 1);
4051 assert_eq!(setup.main.counts.num_tx_files, 1);
4052 assert_eq!(setup.main.counts.num_delete_files, 0);
4053 assert_eq!(setup.main.counts.num_index_files, 7);
4054 assert_eq!(setup.branch1.counts.num_manifest_files, 1);
4055 assert_eq!(setup.branch1.counts.num_data_files, 1);
4056 assert_eq!(setup.branch1.counts.num_tx_files, 1);
4057 assert_eq!(setup.branch1.counts.num_delete_files, 0);
4058 assert_eq!(setup.branch1.counts.num_index_files, 7);
4059 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
4060 assert_eq!(setup.branch2.counts.num_data_files, 1);
4061 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4062 assert_eq!(setup.branch2.counts.num_delete_files, 0);
4063 assert_eq!(setup.branch2.counts.num_index_files, 7);
4064 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
4065 assert_eq!(setup.branch3.counts.num_data_files, 1);
4066 assert_eq!(setup.branch3.counts.num_tx_files, 1);
4067 assert_eq!(setup.branch3.counts.num_delete_files, 0);
4068 assert_eq!(setup.branch3.counts.num_index_files, 7);
4069 setup.assert_unchanged(&["branch4"]).await;
4070 }
4071
4072 #[tokio::test]
4073 async fn explain_cleanup_with_referenced_branches_matches_cleanup() {
4074 let mut setup = build_lineage_datasets().await.unwrap();
4075
4076 setup.enable_auto_cleanup().await.unwrap();
4077 setup.main.write_data().await.unwrap();
4078 setup.main.compact().await.unwrap();
4079 setup.branch4.compact().await.unwrap();
4080 setup.branch1.write_data().await.unwrap();
4081 setup.branch1.compact().await.unwrap();
4082 setup.branch2.write_data().await.unwrap();
4083 setup.branch2.compact().await.unwrap();
4084 setup.branch3.write_data().await.unwrap();
4085 setup.branch3.compact().await.unwrap();
4086
4087 setup.main.refresh().await.unwrap();
4088 setup.branch1.refresh().await.unwrap();
4089 setup.branch2.refresh().await.unwrap();
4090 setup.branch3.refresh().await.unwrap();
4091 setup.branch4.refresh().await.unwrap();
4092 let main_counts_before = setup.main.counts;
4093 let branch1_counts_before = setup.branch1.counts;
4094 let branch2_counts_before = setup.branch2.counts;
4095 let branch3_counts_before = setup.branch3.counts;
4096 let branch4_counts_before = setup.branch4.counts;
4097
4098 let explanation = setup
4099 .main
4100 .explain_cleanup_with_referenced_branches()
4101 .await
4102 .unwrap();
4103
4104 setup.main.refresh().await.unwrap();
4105 setup.branch1.refresh().await.unwrap();
4106 setup.branch2.refresh().await.unwrap();
4107 setup.branch3.refresh().await.unwrap();
4108 setup.branch4.refresh().await.unwrap();
4109 assert_eq!(setup.main.counts, main_counts_before);
4110 assert_eq!(setup.branch1.counts, branch1_counts_before);
4111 assert_eq!(setup.branch2.counts, branch2_counts_before);
4112 assert_eq!(setup.branch3.counts, branch3_counts_before);
4113 assert_eq!(setup.branch4.counts, branch4_counts_before);
4114
4115 let removed = setup
4116 .main
4117 .run_cleanup_with_referenced_branches()
4118 .await
4119 .unwrap();
4120
4121 assert!(!explanation.referenced_branches.is_empty());
4122 assert!(
4123 explanation
4124 .referenced_branches
4125 .iter()
4126 .any(|branch| branch.cleanup_candidate)
4127 );
4128 assert_eq!(explanation.stats, removed);
4129 setup.branch1.refresh().await.unwrap();
4130 setup.branch2.refresh().await.unwrap();
4131 setup.branch3.refresh().await.unwrap();
4132 setup.branch4.refresh().await.unwrap();
4133 assert_eq!(setup.main.counts.num_manifest_files, 1);
4134 assert_eq!(setup.branch1.counts.num_manifest_files, 1);
4135 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
4136 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
4137 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
4138 }
4139
4140 #[tokio::test]
4141 async fn auto_clean_referenced_branches_with_tags() {
4142 let mut setup = build_lineage_datasets().await.unwrap();
4143
4144 setup
4145 .branch3
4146 .dataset
4147 .tags()
4148 .create("branch3-tag", setup.branch3.dataset.version().version)
4149 .await
4150 .unwrap();
4151 setup
4152 .main
4153 .dataset
4154 .tags()
4155 .create("main-tag", setup.main.dataset.version().version)
4156 .await
4157 .unwrap();
4158
4159 setup.branch1.compact().await.unwrap();
4160 setup.branch2.compact().await.unwrap();
4161 setup.branch3.compact().await.unwrap();
4162 setup.branch4.compact().await.unwrap();
4163 setup.main.compact().await.unwrap();
4164 setup.enable_auto_cleanup().await.unwrap();
4165 setup
4166 .main
4167 .run_cleanup_with_referenced_branches()
4168 .await
4169 .unwrap();
4170 setup.branch1.refresh().await.unwrap();
4171 setup.branch2.refresh().await.unwrap();
4172 setup.branch3.refresh().await.unwrap();
4173 setup.branch4.refresh().await.unwrap();
4174 assert_eq!(setup.main.counts.num_manifest_files, 3);
4177 assert_eq!(setup.main.counts.num_data_files, 4);
4178 assert_eq!(setup.main.counts.num_tx_files, 2);
4179 assert_eq!(setup.main.counts.num_delete_files, 2);
4180 assert_eq!(setup.main.counts.num_index_files, 14);
4181 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
4183 assert_eq!(setup.branch2.counts.num_data_files, 2);
4184 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4185 assert_eq!(setup.branch2.counts.num_delete_files, 1);
4186 assert_eq!(setup.branch2.counts.num_index_files, 7);
4187 assert_eq!(setup.branch2.counts.num_manifest_files, 2);
4189 assert_eq!(setup.branch2.counts.num_data_files, 2);
4190 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4191 assert_eq!(setup.branch2.counts.num_delete_files, 1);
4192 assert_eq!(setup.branch2.counts.num_index_files, 7);
4193 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
4194 assert_eq!(setup.branch4.counts.num_data_files, 1);
4195 assert_eq!(setup.branch4.counts.num_tx_files, 1);
4196 assert_eq!(setup.branch4.counts.num_delete_files, 0);
4197 assert_eq!(setup.branch4.counts.num_index_files, 7);
4198
4199 setup
4200 .branch3
4201 .dataset
4202 .tags()
4203 .delete("branch3-tag")
4204 .await
4205 .unwrap();
4206 setup
4207 .main
4208 .run_cleanup_with_referenced_branches()
4209 .await
4210 .unwrap();
4211 setup.branch1.refresh().await.unwrap();
4212 setup.branch2.refresh().await.unwrap();
4213 setup.branch3.refresh().await.unwrap();
4214 setup.branch4.refresh().await.unwrap();
4215 assert_eq!(setup.main.counts.num_manifest_files, 2);
4217 assert_eq!(setup.main.counts.num_data_files, 4);
4218 assert_eq!(setup.main.counts.num_tx_files, 2);
4219 assert_eq!(setup.main.counts.num_delete_files, 2);
4220 assert_eq!(setup.main.counts.num_index_files, 14);
4221 assert_eq!(setup.branch1.counts.num_manifest_files, 1);
4222 assert_eq!(setup.branch1.counts.num_data_files, 1);
4223 assert_eq!(setup.branch1.counts.num_tx_files, 1);
4224 assert_eq!(setup.branch1.counts.num_delete_files, 0);
4225 assert_eq!(setup.branch1.counts.num_index_files, 7);
4226 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
4227 assert_eq!(setup.branch2.counts.num_data_files, 1);
4228 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4229 assert_eq!(setup.branch2.counts.num_delete_files, 0);
4230 assert_eq!(setup.branch2.counts.num_index_files, 7);
4231 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
4232 assert_eq!(setup.branch3.counts.num_data_files, 1);
4233 assert_eq!(setup.branch3.counts.num_tx_files, 1);
4234 assert_eq!(setup.branch3.counts.num_delete_files, 0);
4235 assert_eq!(setup.branch3.counts.num_index_files, 7);
4236 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
4237 assert_eq!(setup.branch4.counts.num_data_files, 1);
4238 assert_eq!(setup.branch4.counts.num_tx_files, 1);
4239 assert_eq!(setup.branch4.counts.num_delete_files, 0);
4240 assert_eq!(setup.branch4.counts.num_index_files, 7);
4241
4242 setup.main.dataset.tags().delete("main-tag").await.unwrap();
4243 setup
4244 .main
4245 .run_cleanup_with_referenced_branches()
4246 .await
4247 .unwrap();
4248 setup.branch2.refresh().await.unwrap();
4249 setup.branch3.refresh().await.unwrap();
4250 setup.branch4.refresh().await.unwrap();
4251 assert_eq!(setup.main.counts.num_manifest_files, 1);
4253 assert_eq!(setup.main.counts.num_data_files, 1);
4254 assert_eq!(setup.main.counts.num_tx_files, 1);
4255 assert_eq!(setup.main.counts.num_delete_files, 0);
4256 assert_eq!(setup.main.counts.num_index_files, 7);
4257 assert_eq!(setup.branch2.counts.num_manifest_files, 1);
4258 assert_eq!(setup.branch2.counts.num_data_files, 1);
4259 assert_eq!(setup.branch2.counts.num_tx_files, 1);
4260 assert_eq!(setup.branch2.counts.num_delete_files, 0);
4261 assert_eq!(setup.branch2.counts.num_index_files, 7);
4262 assert_eq!(setup.branch3.counts.num_manifest_files, 1);
4263 assert_eq!(setup.branch3.counts.num_data_files, 1);
4264 assert_eq!(setup.branch3.counts.num_tx_files, 1);
4265 assert_eq!(setup.branch3.counts.num_delete_files, 0);
4266 assert_eq!(setup.branch3.counts.num_index_files, 7);
4267 assert_eq!(setup.branch4.counts.num_manifest_files, 1);
4268 assert_eq!(setup.branch4.counts.num_data_files, 1);
4269 assert_eq!(setup.branch4.counts.num_tx_files, 1);
4270 assert_eq!(setup.branch4.counts.num_delete_files, 0);
4271 assert_eq!(setup.branch4.counts.num_index_files, 7);
4272 }
4273
4274 #[test]
4275 fn test_calculate_duration_s3() {
4276 let normal_rate = 100;
4278 let expected_duration_ns =
4279 1_000_000_000u64.div_ceil(normal_rate * S3_DELETE_STREAM_BATCH_SIZE);
4280 assert_eq!(
4281 calculate_duration("s3".to_string(), normal_rate),
4282 Duration::from_nanos(expected_duration_ns)
4283 );
4284
4285 let min_rate_duration = calculate_duration("s3".to_string(), 1);
4287 assert_eq!(calculate_duration("s3".to_string(), 0), min_rate_duration);
4288
4289 let very_large_rate = 2_000_000;
4291 assert_eq!(
4292 calculate_duration("s3".to_string(), very_large_rate),
4293 Duration::from_nanos(1)
4294 );
4295 }
4296
4297 #[tokio::test]
4298 async fn test_cleanup_with_rate_limit() {
4299 let fixture = MockDatasetFixture::try_new().unwrap();
4301 fixture.create_some_data().await.unwrap();
4302 for _ in 0..4 {
4304 fixture.overwrite_some_data().await.unwrap();
4305 }
4306
4307 MockClock::set_system_time(TimeDelta::try_days(10).unwrap().to_std().unwrap());
4308
4309 let policy = CleanupPolicyBuilder::default()
4311 .before_timestamp(utc_now() - TimeDelta::try_days(8).unwrap())
4312 .delete_rate_limit(1)
4313 .unwrap()
4314 .build();
4315
4316 let start = std::time::Instant::now();
4317 let db = fixture.open().await.unwrap();
4318 let stats = cleanup_old_versions(&db, policy).await.unwrap();
4319 let elapsed = start.elapsed();
4320
4321 assert!(
4323 stats.old_versions > 0,
4324 "expected some old versions to be removed"
4325 );
4326 assert!(
4329 elapsed.as_millis() >= 2000,
4330 "expected cleanup to be rate-limited (elapsed: {:?})",
4331 elapsed
4332 );
4333 }
4334}