1use std::collections::{BTreeMap, HashMap, HashSet};
25use std::sync::Arc;
26use std::time::Duration;
27
28use common_meta::datanode::GcStat;
29use common_telemetry::tracing::Instrument as _;
30use common_telemetry::{debug, error, info, warn};
31use common_time::Timestamp;
32use itertools::Itertools;
33use object_store::{Entry, ErrorKind, Lister};
34use serde::{Deserialize, Serialize};
35use snafu::{ResultExt as _, ensure};
36use store_api::storage::{FileId, FileRef, FileRefsManifest, GcReport, IndexVersion, RegionId};
37use tokio::sync::{OwnedSemaphorePermit, TryAcquireError};
38use tokio_stream::StreamExt;
39
40use crate::access_layer::AccessLayerRef;
41use crate::cache::CacheManagerRef;
42use crate::cache::file_cache::FileType;
43use crate::config::MitoConfig;
44use crate::engine::region_hook::{RegionGcInfo, RegionHookRef};
45use crate::error::{
46 DurationOutOfRangeSnafu, InvalidRequestSnafu, JoinSnafu, OpenDalSnafu, Result,
47 TooManyGcJobsSnafu, UnexpectedSnafu,
48};
49use crate::manifest::action::{RegionManifest, RemovedFile};
50use crate::metrics::{
51 GC_DELETE_FILE_CNT, GC_DURATION_SECONDS, GC_ERRORS_TOTAL, GC_FILES_DELETED_TOTAL,
52 GC_ORPHANED_INDEX_FILES, GC_RUNS_TOTAL, GC_SKIPPED_UNPARSABLE_FILES,
53};
54use crate::region::{MitoRegionRef, RegionRoleState};
55use crate::sst::file::{RegionFileId, RegionIndexId, delete_files, delete_indexes};
56use crate::sst::location::{self};
57use crate::worker::DROPPING_MARKER_FILE;
58
59#[cfg(test)]
60mod worker_test;
61
62fn should_delete_file(
65 is_in_manifest: bool,
66 is_in_tmp_ref: bool,
67 is_linger: bool,
68 is_eligible_for_delete: bool,
69 is_region_dropped: bool,
70 entry: &Entry,
71 unknown_file_may_linger_until: chrono::DateTime<chrono::Utc>,
72) -> bool {
73 if is_in_manifest || is_in_tmp_ref {
74 return false;
75 }
76
77 let is_known = is_linger || is_eligible_for_delete;
78 if is_known {
79 return is_eligible_for_delete;
80 }
81
82 if is_region_dropped {
90 return true;
91 }
92
93 entry
94 .metadata()
95 .last_modified()
96 .map(|ts| {
97 ts.into_inner().as_millisecond() < unknown_file_may_linger_until.timestamp_millis()
98 })
99 .unwrap_or(false)
100}
101
102pub struct GcLimiter {
104 pub gc_job_limit: Arc<tokio::sync::Semaphore>,
105 gc_concurrency: usize,
106}
107
108pub type GcLimiterRef = Arc<GcLimiter>;
109
110impl GcLimiter {
111 pub fn new(gc_concurrency: usize) -> Self {
112 Self {
113 gc_job_limit: Arc::new(tokio::sync::Semaphore::new(gc_concurrency)),
114 gc_concurrency,
115 }
116 }
117
118 pub fn running_gc_tasks(&self) -> u32 {
119 (self.gc_concurrency - self.gc_job_limit.available_permits()) as u32
120 }
121
122 pub fn gc_concurrency(&self) -> u32 {
123 self.gc_concurrency as u32
124 }
125
126 pub fn gc_stat(&self) -> GcStat {
127 GcStat::new(self.running_gc_tasks(), self.gc_concurrency())
128 }
129
130 pub fn permit(&self) -> Result<OwnedSemaphorePermit> {
134 self.gc_job_limit
135 .clone()
136 .try_acquire_owned()
137 .map_err(|e| match e {
138 TryAcquireError::Closed => UnexpectedSnafu {
139 reason: format!("Failed to acquire gc permit: {e}"),
140 }
141 .build(),
142 TryAcquireError::NoPermits => TooManyGcJobsSnafu {}.build(),
143 })
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(default)]
149pub struct GcConfig {
150 pub enable: bool,
152 #[serde(with = "humantime_serde")]
157 pub lingering_time: Option<Duration>,
158 #[serde(with = "humantime_serde")]
169 pub unknown_file_lingering_time: Duration,
170 pub max_concurrent_lister_per_gc_job: usize,
173 pub max_concurrent_gc_job: usize,
179}
180
181impl Default for GcConfig {
182 fn default() -> Self {
183 Self {
184 enable: false,
185 lingering_time: Some(Duration::from_secs(60 * 60)),
187 unknown_file_lingering_time: Duration::from_secs(24 * 60 * 60),
192 max_concurrent_lister_per_gc_job: 32,
193 max_concurrent_gc_job: 4,
194 }
195 }
196}
197
198pub struct LocalGcWorker {
199 pub(crate) access_layer: AccessLayerRef,
200 pub(crate) cache_manager: Option<CacheManagerRef>,
201 pub(crate) regions: BTreeMap<RegionId, Option<MitoRegionRef>>,
202 pub(crate) opt: GcConfig,
204 pub(crate) file_ref_manifest: FileRefsManifest,
209 _permit: OwnedSemaphorePermit,
210 pub full_file_listing: bool,
219 pub(crate) hook: Option<RegionHookRef>,
222}
223
224pub struct ManifestOpenConfig {
225 pub compress_manifest: bool,
226 pub manifest_checkpoint_distance: u64,
227 pub experimental_manifest_keep_removed_file_count: usize,
228 pub experimental_manifest_keep_removed_file_ttl: Duration,
229}
230
231impl From<MitoConfig> for ManifestOpenConfig {
232 fn from(mito_config: MitoConfig) -> Self {
233 Self {
234 compress_manifest: mito_config.compress_manifest,
235 manifest_checkpoint_distance: mito_config.manifest_checkpoint_distance,
236 experimental_manifest_keep_removed_file_count: mito_config
237 .experimental_manifest_keep_removed_file_count,
238 experimental_manifest_keep_removed_file_ttl: mito_config
239 .experimental_manifest_keep_removed_file_ttl,
240 }
241 }
242}
243
244impl LocalGcWorker {
245 #[allow(clippy::too_many_arguments)]
249 pub async fn try_new(
250 access_layer: AccessLayerRef,
251 cache_manager: Option<CacheManagerRef>,
252 regions_to_gc: BTreeMap<RegionId, Option<MitoRegionRef>>,
253 opt: GcConfig,
254 file_ref_manifest: FileRefsManifest,
255 limiter: &GcLimiterRef,
256 full_file_listing: bool,
257 hook: Option<RegionHookRef>,
258 ) -> Result<Self> {
259 if let Some(first_region_id) = regions_to_gc.keys().next() {
260 let table_id = first_region_id.table_id();
261 for region_id in regions_to_gc.keys() {
262 ensure!(
263 region_id.table_id() == table_id,
264 InvalidRequestSnafu {
265 region_id: *region_id,
266 reason: format!(
267 "Region {} does not belong to table {}",
268 region_id, table_id
269 ),
270 }
271 );
272 }
273 }
274
275 let permit = limiter.permit()?;
276
277 Ok(Self {
278 access_layer,
279 cache_manager,
280 regions: regions_to_gc,
281 opt,
282 file_ref_manifest,
283 _permit: permit,
284 full_file_listing,
285 hook,
286 })
287 }
288
289 pub async fn read_tmp_ref_files(&self) -> Result<HashMap<RegionId, HashSet<FileRef>>> {
291 let mut tmp_ref_files = HashMap::new();
292 for (region_id, file_refs) in &self.file_ref_manifest.file_refs {
293 tmp_ref_files
294 .entry(*region_id)
295 .or_insert_with(HashSet::new)
296 .extend(file_refs.clone());
297 }
299
300 Ok(tmp_ref_files)
301 }
302
303 #[common_telemetry::tracing::instrument(
309 skip_all,
310 fields(region_count = self.regions.len(), full_file_listing = self.full_file_listing)
311 )]
312 pub async fn run(self) -> Result<GcReport> {
313 info!("LocalGcWorker started");
314 let _timer = GC_DURATION_SECONDS
315 .with_label_values(&["total"])
316 .start_timer();
317 let now = std::time::Instant::now();
318
319 let mut deleted_files = HashMap::new();
320 let mut deleted_indexes = HashMap::new();
321 let mut processed_regions = HashSet::new();
322 let mut need_retry_regions = HashSet::new();
323 let tmp_ref_files = self.read_tmp_ref_files().await?;
324 for (region_id, region) in &self.regions {
325 let per_region_time = std::time::Instant::now();
326 if region.as_ref().map(|r| r.manifest_ctx.current_state())
327 == Some(RegionRoleState::Follower)
328 {
329 return UnexpectedSnafu {
330 reason: format!(
331 "Region {} is in Follower state, should not run GC on follower regions",
332 region_id
333 ),
334 }
335 .fail();
336 }
337 let tmp_ref_files = tmp_ref_files
338 .get(region_id)
339 .cloned()
340 .unwrap_or_else(HashSet::new);
341 let outcome = self
342 .do_region_gc(*region_id, region.clone(), &tmp_ref_files)
343 .await?;
344 let RegionGcOutcome {
345 removed_files,
346 extension_cleanup_failed,
347 } = outcome;
348 let index_files = removed_files
349 .iter()
350 .filter_map(|f| f.index_version().map(|v| (f.file_id(), v)))
351 .collect_vec();
352 let data_files = removed_files
353 .into_iter()
354 .filter_map(|f| match f {
355 RemovedFile::File(file_id, _) => Some(file_id),
356 RemovedFile::Index(_, _) => None,
357 })
358 .collect();
359 if extension_cleanup_failed {
362 need_retry_regions.insert(*region_id);
363 } else {
364 deleted_files.insert(*region_id, data_files);
365 deleted_indexes.insert(*region_id, index_files);
366 processed_regions.insert(*region_id);
367 }
368 debug!(
369 "GC for region {} took {} secs.",
370 region_id,
371 per_region_time.elapsed().as_secs_f32()
372 );
373 }
374 info!(
375 "LocalGcWorker finished after {} secs.",
376 now.elapsed().as_secs_f32()
377 );
378 let report = GcReport {
379 deleted_files,
380 deleted_indexes,
381 need_retry_regions,
382 processed_regions,
383 };
384 Ok(report)
385 }
386}
387
388pub(crate) struct RegionGcOutcome {
394 pub removed_files: Vec<RemovedFile>,
396 pub extension_cleanup_failed: bool,
399}
400
401impl LocalGcWorker {
402 pub const CONCURRENCY_LIST_PER_FILES: usize = 1024;
405
406 #[common_telemetry::tracing::instrument(
415 skip_all,
416 fields(
417 region_id = %region_id,
418 full_file_listing = self.full_file_listing,
419 region_present = region.is_some()
420 )
421 )]
422 pub(crate) async fn do_region_gc(
423 &self,
424 region_id: RegionId,
425 region: Option<MitoRegionRef>,
426 tmp_ref_files: &HashSet<FileRef>,
427 ) -> Result<RegionGcOutcome> {
428 let mode = if self.full_file_listing {
429 "full_listing"
430 } else {
431 "fast"
432 };
433 GC_RUNS_TOTAL.with_label_values(&[mode]).inc();
434 debug!(
435 "Doing gc for region {}, {}",
436 region_id,
437 if region.is_some() {
438 "region found"
439 } else {
440 "region not found, might be dropped"
441 }
442 );
443
444 ensure!(
445 region.is_some() || self.full_file_listing,
446 InvalidRequestSnafu {
447 region_id,
448 reason: "region not found and full_file_listing is false; refusing GC without full listing".to_string(),
449 }
450 );
451
452 let manifest = if let Some(region) = ®ion {
453 let manifest = region.manifest_ctx.manifest().await;
454 let file_ref_manifest_version = self
456 .file_ref_manifest
457 .manifest_version
458 .get(®ion.region_id())
459 .cloned();
460 if file_ref_manifest_version != Some(manifest.manifest_version) {
461 warn!(
463 "Tmp ref files manifest version {:?} does not match region {} manifest version {}, skip gc for this region",
464 file_ref_manifest_version,
465 region.region_id(),
466 manifest.manifest_version
467 );
468 GC_ERRORS_TOTAL
469 .with_label_values(&["manifest_mismatch"])
470 .inc();
471 return Ok(RegionGcOutcome {
472 removed_files: vec![],
473 extension_cleanup_failed: false,
474 });
475 }
476 Some(manifest)
477 } else {
478 None
479 };
480
481 let all_entries = if let Some(manifest) = &manifest
482 && self.full_file_listing
483 {
484 self.list_from_object_store(region_id, manifest.files.len())
487 .await?
488 } else if manifest.is_none() && self.full_file_listing {
489 self.list_from_object_store(region_id, Self::CONCURRENCY_LIST_PER_FILES)
493 .await?
494 } else {
495 vec![]
496 };
497
498 let recently_removed_files = if let Some(manifest) = &manifest {
499 self.get_removed_files_expel_times(manifest).await?
500 } else {
501 Default::default()
502 };
503
504 if recently_removed_files.is_empty() {
505 debug!("No recently removed files to gc for region {}", region_id);
507 }
508
509 let removed_file_cnt = recently_removed_files
510 .values()
511 .map(|s| s.len())
512 .sum::<usize>();
513
514 let current_files = manifest.as_ref().map(|m| &m.files);
515
516 let in_manifest = if let Some(current_files) = current_files {
517 current_files
518 .iter()
519 .map(|(file_id, meta)| (*file_id, meta.index_version()))
520 .collect::<HashMap<_, _>>()
521 } else {
522 Default::default()
523 };
524
525 let is_region_dropped = region.is_none();
526
527 let in_tmp_ref = tmp_ref_files
528 .iter()
529 .map(|file_ref| (file_ref.file_id, file_ref.index_version))
530 .collect::<HashSet<_>>();
531
532 let deletable_files = self
533 .list_to_be_deleted_files(
534 region_id,
535 is_region_dropped,
536 &in_manifest,
537 &in_tmp_ref,
538 recently_removed_files,
539 all_entries,
540 )
541 .await?;
542
543 let unused_file_cnt = deletable_files.len();
544
545 info!(
546 "gc: for region{}{region_id}: In manifest file cnt: {}, Tmp ref file cnt: {}, recently removed files: {}, Unused files to delete count: {}",
547 if region.is_none() {
548 "(region dropped)"
549 } else {
550 ""
551 },
552 current_files.map(|c| c.len()).unwrap_or(0),
553 tmp_ref_files.len(),
554 removed_file_cnt,
555 deletable_files.len(),
556 );
557 debug!(
558 "gc: deletable files for region {}: {:?}",
559 region_id, &deletable_files
560 );
561
562 debug!(
563 "Found {} unused index files to delete for region {}",
564 deletable_files.len(),
565 region_id
566 );
567
568 let _delete_timer = GC_DURATION_SECONDS
569 .with_label_values(&["delete_files"])
570 .start_timer();
571 self.delete_files(region_id, &deletable_files).await?;
572
573 debug!(
574 "Successfully deleted {} unused files for region {}",
575 unused_file_cnt, region_id
576 );
577
578 let extension_cleanup_failed = if let Some(hook) = &self.hook
583 && (!deletable_files.is_empty() || self.full_file_listing)
584 {
585 let region_metadata = region.as_ref().map(|r| r.metadata());
586 let result = hook
587 .on_region_gc(
588 region_id,
589 region_metadata.as_ref(),
590 &self.access_layer,
591 &RegionGcInfo {
592 removed_files: &deletable_files,
593 is_region_dropped,
594 full_file_listing: self.full_file_listing,
595 },
596 )
597 .await;
598 if let Err(err) = result {
599 warn!(
600 err;
601 "Region hook on_region_gc failed for region {}, will retry on the next GC pass",
602 region_id
603 );
604 true
605 } else {
606 false
607 }
608 } else {
609 false
610 };
611
612 if !extension_cleanup_failed && let Some(region) = ®ion {
616 let _update_timer = GC_DURATION_SECONDS
617 .with_label_values(&["update_manifest"])
618 .start_timer();
619 self.update_manifest_removed_files(region, deletable_files.clone())
620 .await?;
621 }
622
623 Ok(RegionGcOutcome {
624 removed_files: deletable_files,
625 extension_cleanup_failed,
626 })
627 }
628
629 #[common_telemetry::tracing::instrument(
630 skip_all,
631 fields(region_id = %region_id, removed_file_count = removed_files.len())
632 )]
633 async fn delete_files(&self, region_id: RegionId, removed_files: &[RemovedFile]) -> Result<()> {
634 let mut index_ids = vec![];
635 let file_pairs = removed_files
636 .iter()
637 .filter_map(|f| match f {
638 RemovedFile::File(file_id, v) => Some((*file_id, v.unwrap_or(0))),
639 RemovedFile::Index(file_id, index_version) => {
640 let region_index_id =
641 RegionIndexId::new(RegionFileId::new(region_id, *file_id), *index_version);
642 index_ids.push(region_index_id);
643 None
644 }
645 })
646 .collect_vec();
647 delete_files(
648 region_id,
649 &file_pairs,
650 true,
651 &self.access_layer,
652 &self.cache_manager,
653 )
654 .await?;
655
656 if !file_pairs.is_empty() {
657 let deleted_count = file_pairs.len() as u64;
658 GC_FILES_DELETED_TOTAL
659 .with_label_values(&["parquet"])
660 .inc_by(deleted_count);
661 GC_DELETE_FILE_CNT.inc_by(deleted_count);
662 }
663
664 if !index_ids.is_empty() {
665 let deleted_count = index_ids.len() as u64;
666 delete_indexes(&index_ids, &self.access_layer, &self.cache_manager)
667 .await
668 .inspect_err(|_| {
669 GC_ERRORS_TOTAL.with_label_values(&["delete_failed"]).inc();
670 })?;
671 GC_FILES_DELETED_TOTAL
672 .with_label_values(&["index"])
673 .inc_by(deleted_count);
674 GC_DELETE_FILE_CNT.inc_by(deleted_count);
675 }
676
677 Ok(())
678 }
679
680 #[common_telemetry::tracing::instrument(
682 skip_all,
683 fields(region_id = %region.region_id(), deleted_file_count = deleted_files.len())
684 )]
685 async fn update_manifest_removed_files(
686 &self,
687 region: &MitoRegionRef,
688 deleted_files: Vec<RemovedFile>,
689 ) -> Result<()> {
690 let deleted_file_cnt = deleted_files.len();
691 debug!(
692 "Trying to update manifest for {deleted_file_cnt} removed files for region {}",
693 region.region_id()
694 );
695
696 let mut manager = region.manifest_ctx.manifest_manager.write().await;
697 let cnt = deleted_files.len();
698 manager.clear_deleted_files(deleted_files);
699 debug!(
700 "Updated region_id={} region manifest to clear {cnt} deleted files",
701 region.region_id(),
702 );
703
704 Ok(())
705 }
706
707 pub async fn get_removed_files_expel_times(
712 &self,
713 region_manifest: &Arc<RegionManifest>,
714 ) -> Result<BTreeMap<Timestamp, HashSet<RemovedFile>>> {
715 let mut ret = BTreeMap::new();
716 for files in ®ion_manifest.removed_files.removed_files {
717 let expel_time = Timestamp::new_millisecond(files.removed_at);
718 let set = ret.entry(expel_time).or_insert_with(HashSet::new);
719 set.extend(files.files.iter().cloned());
720 }
721
722 Ok(ret)
723 }
724
725 async fn partition_region_files(
728 &self,
729 region_id: RegionId,
730 concurrency: usize,
731 ) -> Result<Vec<(Lister, Option<String>)>> {
732 let region_dir = self.access_layer.build_region_dir(region_id);
733
734 let partitions = gen_partition_from_concurrency(concurrency);
735 let bounds = vec![None]
736 .into_iter()
737 .chain(partitions.iter().map(|p| Some(p.clone())))
738 .chain(vec![None])
739 .collect::<Vec<_>>();
740
741 let mut listers = vec![];
742 for part in bounds.windows(2) {
743 let start = part[0].clone();
744 let end = part[1].clone();
745 let mut lister = self.access_layer.object_store().lister_with(®ion_dir);
746 if let Some(s) = start {
747 lister = lister.start_after(&s);
748 }
749
750 let lister = lister.await.context(OpenDalSnafu)?;
751 listers.push((lister, end));
752 }
753
754 Ok(listers)
755 }
756
757 #[common_telemetry::tracing::instrument(
761 skip_all,
762 fields(region_id = %region_id, file_cnt_hint = file_cnt_hint)
763 )]
764 async fn list_from_object_store(
765 &self,
766 region_id: RegionId,
767 file_cnt_hint: usize,
768 ) -> Result<Vec<Entry>> {
769 let _timer = GC_DURATION_SECONDS
770 .with_label_values(&["list_files"])
771 .start_timer();
772 let start = tokio::time::Instant::now();
773 let concurrency = (file_cnt_hint / Self::CONCURRENCY_LIST_PER_FILES)
774 .max(1)
775 .min(self.opt.max_concurrent_lister_per_gc_job);
776
777 let listers = self
778 .partition_region_files(region_id, concurrency)
779 .await
780 .inspect_err(|_| {
781 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
782 })?;
783 let lister_cnt = listers.len();
784
785 let mut all_entries = self
787 .list_region_files_concurrent(listers)
788 .await
789 .inspect_err(|_| {
790 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
791 })?;
792 let root_cnt = all_entries.len();
793
794 let index_entries = self
798 .list_region_index_files(region_id)
799 .await
800 .inspect_err(|_| {
801 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
802 })?;
803 let index_cnt = index_entries.len();
804 all_entries.extend(index_entries);
805 info!(
806 "gc: full listing mode cost {} secs using {lister_cnt} lister for root={root_cnt} index={index_cnt} files in region {}.",
807 start.elapsed().as_secs_f64(),
808 region_id
809 );
810 Ok(all_entries)
811 }
812
813 async fn list_region_index_files(&self, region_id: RegionId) -> Result<Vec<Entry>> {
817 let region_dir = self.access_layer.build_region_dir(region_id);
818 let index_dir = location::index_dir(®ion_dir);
819
820 let mut lister = match self
821 .access_layer
822 .object_store()
823 .lister_with(&index_dir)
824 .await
825 {
826 Ok(l) => l,
827 Err(e) if e.kind() == ErrorKind::NotFound => {
828 debug!(
832 "Index directory not found for region {}: {}. Treating as empty.",
833 region_id, e
834 );
835 return Ok(vec![]);
836 }
837 Err(e) => return Err(e).context(OpenDalSnafu),
838 };
839
840 let mut entries = Vec::new();
841 while let Some(entry) = lister.next().await {
842 let entry = entry.context(OpenDalSnafu)?;
843 if entry.metadata().is_file() && entry.name().ends_with(".puffin") {
844 entries.push(entry);
845 }
846 }
847
848 Ok(entries)
849 }
850
851 async fn list_region_files_concurrent(
854 &self,
855 listers: Vec<(Lister, Option<String>)>,
856 ) -> Result<Vec<Entry>> {
857 let (tx, mut rx) = tokio::sync::mpsc::channel(1024);
858 let mut handles = vec![];
859
860 for (lister, end) in listers {
861 let tx = tx.clone();
862 let handle = tokio::spawn(
863 async move {
864 let stream = lister.take_while(|e: &std::result::Result<Entry, _>| match e {
865 Ok(e) => {
866 if let Some(end) = &end {
867 e.name() < end.as_str()
869 } else {
870 true
872 }
873 }
874 Err(err) => {
877 warn!("Failed to list entry: {}", err);
878 true
879 }
880 });
881 let stream = stream
882 .filter(|e| {
883 if let Ok(e) = &e {
884 e.metadata().is_file()
886 } else {
887 true
889 }
890 })
891 .collect::<Vec<_>>()
892 .await;
893 tx.send(stream).await.expect("Failed to send entries");
895 }
896 .instrument(common_telemetry::tracing::info_span!("gc_list_partition")),
897 );
898
899 handles.push(handle);
900 }
901
902 for handle in handles {
904 handle.await.context(JoinSnafu)?;
905 }
906
907 drop(tx); let mut all_entries = vec![];
911 while let Some(stream) = rx.recv().await {
912 for entry in stream {
913 all_entries.push(entry.context(OpenDalSnafu)?);
914 }
915 }
916
917 Ok(all_entries)
918 }
919
920 #[allow(clippy::too_many_arguments)]
921 fn filter_deletable_files(
922 &self,
923 is_region_dropped: bool,
924 entries: Vec<Entry>,
925 in_manifest: &HashMap<FileId, Option<IndexVersion>>,
926 in_tmp_ref: &HashSet<(FileId, Option<IndexVersion>)>,
927 may_linger_files: &HashSet<&RemovedFile>,
928 eligible_for_delete: &HashSet<&RemovedFile>,
929 unknown_file_may_linger_until: chrono::DateTime<chrono::Utc>,
930 ) -> Vec<RemovedFile> {
931 let mut ready_for_delete = vec![];
932 let referenced_ssts: HashSet<_> = in_tmp_ref.iter().map(|(file_id, _)| *file_id).collect();
934
935 let may_linger_files: HashMap<FileId, HashSet<&RemovedFile>> = may_linger_files
936 .iter()
937 .fold(HashMap::new(), |mut acc, file| {
938 let indices = acc.entry(file.file_id()).or_default();
939 indices.insert(file);
940 acc
941 });
942
943 let eligible_for_delete: HashMap<FileId, HashSet<&RemovedFile>> = eligible_for_delete
944 .iter()
945 .fold(HashMap::new(), |mut acc, file| {
946 let indices = acc.entry(file.file_id()).or_default();
947 indices.insert(file);
948 acc
949 });
950
951 for entry in entries {
952 if entry.name() == DROPPING_MARKER_FILE {
953 continue;
954 }
955
956 let (file_id, file_type) = match location::parse_file_id_type_from_path(entry.name()) {
957 Ok((file_id, file_type)) => (file_id, file_type),
958 Err(err) => {
959 error!(err; "Failed to parse file id from path: {}", entry.name());
960 GC_SKIPPED_UNPARSABLE_FILES.inc();
963 continue;
964 }
965 };
966
967 let should_delete = match file_type {
968 FileType::Parquet => {
969 let is_in_manifest = in_manifest.contains_key(&file_id);
970 let is_in_tmp_ref = referenced_ssts.contains(&file_id);
971 let is_linger = may_linger_files.contains_key(&file_id);
972 let is_eligible_for_delete = eligible_for_delete.contains_key(&file_id);
973
974 should_delete_file(
975 is_in_manifest,
976 is_in_tmp_ref,
977 is_linger,
978 is_eligible_for_delete,
979 is_region_dropped,
980 &entry,
981 unknown_file_may_linger_until,
982 )
983 }
984 FileType::Puffin(version) => {
985 let is_in_manifest = in_manifest.get(&file_id) == Some(&Some(version));
986 let is_in_tmp_ref = in_tmp_ref.contains(&(file_id, Some(version)));
987 let is_linger = may_linger_files
988 .get(&file_id)
989 .map(|files| files.contains(&&RemovedFile::Index(file_id, version)))
990 .unwrap_or(false);
991 let is_eligible_for_delete = eligible_for_delete
992 .get(&file_id)
993 .map(|files| files.contains(&&RemovedFile::Index(file_id, version)))
994 .unwrap_or(false);
995
996 should_delete_file(
997 is_in_manifest,
998 is_in_tmp_ref,
999 is_linger,
1000 is_eligible_for_delete,
1001 is_region_dropped,
1002 &entry,
1003 unknown_file_may_linger_until,
1004 )
1005 }
1006 };
1007
1008 if should_delete {
1009 let removed_file = match file_type {
1010 FileType::Parquet => {
1011 RemovedFile::File(file_id, None)
1014 }
1015 FileType::Puffin(version) => {
1016 GC_ORPHANED_INDEX_FILES.inc();
1017 RemovedFile::Index(file_id, version)
1018 }
1019 };
1020 ready_for_delete.push(removed_file);
1021 }
1022 }
1023 ready_for_delete
1024 }
1025
1026 pub async fn list_to_be_deleted_files(
1035 &self,
1036 region_id: RegionId,
1037 is_region_dropped: bool,
1038 in_manifest: &HashMap<FileId, Option<IndexVersion>>,
1039 in_tmp_ref: &HashSet<(FileId, Option<IndexVersion>)>,
1040 recently_removed_files: BTreeMap<Timestamp, HashSet<RemovedFile>>,
1041 all_entries: Vec<Entry>,
1042 ) -> Result<Vec<RemovedFile>> {
1043 let now = chrono::Utc::now();
1044 let may_linger_until = self
1045 .opt
1046 .lingering_time
1047 .map(|lingering_time| {
1048 chrono::Duration::from_std(lingering_time)
1049 .with_context(|_| DurationOutOfRangeSnafu {
1050 input: lingering_time,
1051 })
1052 .map(|t| now - t)
1053 })
1054 .transpose()?;
1055
1056 let unknown_file_may_linger_until = now
1057 - chrono::Duration::from_std(self.opt.unknown_file_lingering_time).with_context(
1058 |_| DurationOutOfRangeSnafu {
1059 input: self.opt.unknown_file_lingering_time,
1060 },
1061 )?;
1062
1063 let threshold =
1065 may_linger_until.map(|until| Timestamp::new_millisecond(until.timestamp_millis()));
1066 let mut recently_removed_files = recently_removed_files;
1069 let may_linger_files = match threshold {
1070 Some(threshold) => recently_removed_files.split_off(&threshold),
1071 None => BTreeMap::new(),
1072 };
1073 debug!("may_linger_files: {:?}", may_linger_files);
1074
1075 let all_may_linger_files = may_linger_files.values().flatten().collect::<HashSet<_>>();
1076
1077 let eligible_for_removal = recently_removed_files
1080 .values()
1081 .flatten()
1082 .collect::<HashSet<_>>();
1083
1084 if !self.full_file_listing {
1087 let referenced_ssts: HashSet<_> =
1088 in_tmp_ref.iter().map(|(file_id, _)| *file_id).collect();
1089 let files_to_delete: Vec<RemovedFile> = eligible_for_removal
1094 .iter()
1095 .filter(|file| {
1096 let in_use = match file {
1097 RemovedFile::File(file_id, _) => {
1098 in_manifest.contains_key(file_id) || referenced_ssts.contains(file_id)
1099 }
1100 RemovedFile::Index(file_id, version) => {
1101 in_manifest.get(file_id) == Some(&Some(*version))
1102 || in_tmp_ref.contains(&(*file_id, Some(*version)))
1103 }
1104 };
1105 !in_use
1106 })
1107 .map(|&f| f.clone())
1108 .collect();
1109
1110 info!(
1111 "gc: fast mode (no full listing) for region {}, found {} files to delete from manifest",
1112 region_id,
1113 files_to_delete.len()
1114 );
1115
1116 return Ok(files_to_delete);
1117 }
1118
1119 let all_unused_files_ready_for_delete = self.filter_deletable_files(
1123 is_region_dropped,
1124 all_entries,
1125 in_manifest,
1126 in_tmp_ref,
1127 &all_may_linger_files,
1128 &eligible_for_removal,
1129 unknown_file_may_linger_until,
1130 );
1131
1132 Ok(all_unused_files_ready_for_delete)
1133 }
1134}
1135
1136fn gen_partition_from_concurrency(concurrency: usize) -> Vec<String> {
1146 let n = concurrency.next_power_of_two();
1147 if n <= 1 {
1148 return vec![];
1149 }
1150
1151 let mut d = 0;
1155 let mut p: u128 = 1;
1156 while p < n as u128 {
1157 p *= 16;
1158 d += 1;
1159 }
1160
1161 let total_space = p;
1162 let step = total_space / n as u128;
1163
1164 (1..n)
1165 .map(|i| {
1166 let boundary = i as u128 * step;
1167 format!("{:0width$x}", boundary, width = d)
1168 })
1169 .collect()
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175
1176 #[test]
1177 fn test_gen_partition_from_concurrency() {
1178 let partitions = gen_partition_from_concurrency(1);
1179 assert!(partitions.is_empty());
1180
1181 let partitions = gen_partition_from_concurrency(2);
1182 assert_eq!(partitions, vec!["8"]);
1183
1184 let partitions = gen_partition_from_concurrency(3);
1185 assert_eq!(partitions, vec!["4", "8", "c"]);
1186
1187 let partitions = gen_partition_from_concurrency(4);
1188 assert_eq!(partitions, vec!["4", "8", "c"]);
1189
1190 let partitions = gen_partition_from_concurrency(8);
1191 assert_eq!(partitions, vec!["2", "4", "6", "8", "a", "c", "e"]);
1192
1193 let partitions = gen_partition_from_concurrency(16);
1194 assert_eq!(
1195 partitions,
1196 vec![
1197 "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"
1198 ]
1199 );
1200
1201 let partitions = gen_partition_from_concurrency(32);
1202 assert_eq!(
1203 partitions,
1204 [
1205 "08", "10", "18", "20", "28", "30", "38", "40", "48", "50", "58", "60", "68", "70",
1206 "78", "80", "88", "90", "98", "a0", "a8", "b0", "b8", "c0", "c8", "d0", "d8", "e0",
1207 "e8", "f0", "f8",
1208 ]
1209 );
1210 }
1211
1212 #[test]
1213 fn test_gc_config_default_lingering_time() {
1214 assert_eq!(
1215 GcConfig::default().lingering_time,
1216 Some(Duration::from_secs(60 * 60))
1217 );
1218 }
1219}