Skip to main content

risingwave_meta/hummock/manager/
gc.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cmp;
16use std::collections::{HashMap, HashSet};
17use std::ops::DerefMut;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::time::{Duration, Instant, SystemTime};
20
21use chrono::DateTime;
22use futures::future::try_join_all;
23use futures::{StreamExt, TryStreamExt, future};
24use itertools::Itertools;
25use risingwave_common::catalog::TableId;
26use risingwave_common::system_param::reader::SystemParamsRead;
27use risingwave_common::util::epoch::Epoch;
28use risingwave_hummock_sdk::{
29    HummockEpoch, HummockObjectId, HummockRawObjectId, VALID_OBJECT_ID_SUFFIXES,
30    get_object_data_path, get_object_id_from_path,
31};
32use risingwave_meta_model::hummock_sequence::HUMMOCK_NOW;
33use risingwave_meta_model::{hummock_gc_history, hummock_sequence, hummock_version_delta};
34use risingwave_meta_model_migration::OnConflict;
35use risingwave_object_store::object::{ObjectMetadataIter, ObjectStoreRef};
36use risingwave_pb::stream_service::GetMinUncommittedObjectIdRequest;
37use risingwave_rpc_client::StreamClientPool;
38use sea_orm::{ActiveValue, ColumnTrait, EntityTrait, QueryFilter, Set};
39
40use crate::MetaResult;
41use crate::backup_restore::BackupManagerRef;
42use crate::hummock::HummockManager;
43use crate::hummock::error::{Error, Result};
44use crate::manager::MetadataManager;
45
46pub(super) mod change_log_retention;
47
48use change_log_retention::ChangeLogObjectRetention;
49
50pub(crate) struct GcManager {
51    pub(super) change_log_retention: ChangeLogObjectRetention,
52    store: ObjectStoreRef,
53    path_prefix: String,
54    use_new_object_prefix_strategy: bool,
55    /// These objects may still be used by backup or time travel.
56    may_delete_object_ids: parking_lot::Mutex<HashSet<HummockObjectId>>,
57}
58
59impl GcManager {
60    pub fn new(
61        store: ObjectStoreRef,
62        path_prefix: &str,
63        use_new_object_prefix_strategy: bool,
64    ) -> Self {
65        Self {
66            store,
67            path_prefix: path_prefix.to_owned(),
68            use_new_object_prefix_strategy,
69            may_delete_object_ids: Default::default(),
70            change_log_retention: Default::default(),
71        }
72    }
73
74    /// Deletes all objects specified in the given list of IDs from storage.
75    pub async fn delete_objects(
76        &self,
77        object_id_list: impl Iterator<Item = HummockObjectId>,
78    ) -> Result<()> {
79        let mut paths = Vec::with_capacity(1000);
80        for object_id in object_id_list {
81            let obj_prefix = self.store.get_object_prefix(
82                object_id.as_raw().as_raw_id(),
83                self.use_new_object_prefix_strategy,
84            );
85            paths.push(get_object_data_path(
86                &obj_prefix,
87                &self.path_prefix,
88                object_id,
89            ));
90        }
91        self.store.delete_objects(&paths).await?;
92        Ok(())
93    }
94
95    async fn list_object_metadata_from_object_store(
96        &self,
97        prefix: Option<String>,
98        start_after: Option<String>,
99        limit: Option<usize>,
100    ) -> Result<ObjectMetadataIter> {
101        let list_path = format!("{}/{}", self.path_prefix, prefix.unwrap_or("".into()));
102        let raw_iter = self.store.list(&list_path, start_after, limit).await?;
103        let valid_suffixes = VALID_OBJECT_ID_SUFFIXES.map(|suffix| format!(".{}", suffix));
104        let iter = raw_iter.filter(move |r| match r {
105            Ok(i) => future::ready(valid_suffixes.iter().any(|suffix| i.key.ends_with(suffix))),
106            Err(_) => future::ready(true),
107        });
108        Ok(Box::pin(iter))
109    }
110
111    /// Returns **filtered** object ids, and **unfiltered** total object count and size.
112    pub async fn list_objects(
113        &self,
114        object_retention_watermark: u64,
115        prefix: Option<String>,
116        start_after: Option<String>,
117        limit: Option<u64>,
118    ) -> Result<(HashSet<HummockObjectId>, u64, u64, Option<String>)> {
119        tracing::debug!(
120            object_retention_watermark,
121            prefix,
122            start_after,
123            limit,
124            "Try to list objects."
125        );
126        let mut total_object_count = 0;
127        let mut total_object_size = 0;
128        let mut next_start_after: Option<String> = None;
129        let metadata_iter = self
130            .list_object_metadata_from_object_store(prefix, start_after, limit.map(|i| i as usize))
131            .await?;
132        let filtered = metadata_iter
133            .filter_map(|r| {
134                let result = match r {
135                    Ok(o) => {
136                        total_object_count += 1;
137                        total_object_size += o.total_size;
138                        // Determine if the LIST has been truncated.
139                        // A false positives would at most cost one additional LIST later.
140                        if let Some(limit) = limit
141                            && limit == total_object_count
142                        {
143                            next_start_after = Some(o.key.clone());
144                            tracing::debug!(next_start_after, "set next start after");
145                        }
146                        if o.last_modified < object_retention_watermark as f64 {
147                            Some(Ok(get_object_id_from_path(&o.key)))
148                        } else {
149                            None
150                        }
151                    }
152                    Err(e) => Some(Err(Error::ObjectStore(e))),
153                };
154                async move { result }
155            })
156            .try_collect::<HashSet<HummockObjectId>>()
157            .await?;
158        Ok((
159            filtered,
160            total_object_count,
161            total_object_size as u64,
162            next_start_after,
163        ))
164    }
165
166    pub fn add_may_delete_object_ids(
167        &self,
168        may_delete_object_ids: impl Iterator<Item = HummockObjectId>,
169    ) {
170        self.may_delete_object_ids
171            .lock()
172            .extend(may_delete_object_ids);
173    }
174
175    /// Takes if `least_count` elements available.
176    pub fn try_take_may_delete_object_ids(
177        &self,
178        least_count: usize,
179    ) -> Option<HashSet<HummockObjectId>> {
180        let mut guard = self.may_delete_object_ids.lock();
181        if guard.len() < least_count {
182            None
183        } else {
184            Some(std::mem::take(&mut *guard))
185        }
186    }
187}
188
189impl HummockManager {
190    /// Deletes version deltas.
191    ///
192    /// Returns number of deleted deltas
193    pub async fn delete_version_deltas(&self) -> Result<usize> {
194        let mut versioning_guard = self
195            .versioning
196            .write_with_process_name("delete_version_deltas")
197            .await;
198        let versioning = versioning_guard.deref_mut();
199        let context_info = self
200            .context_info
201            .read_with_process_name("delete_version_deltas")
202            .await;
203        // If there is any safe point, skip this to ensure meta backup has required delta logs to
204        // replay version.
205        if !context_info.version_safe_points.is_empty() {
206            return Ok(0);
207        }
208        // The context_info lock must be held to prevent any potential metadata backup.
209        // The lock order requires version lock to be held as well.
210        let version_id = versioning.checkpoint.version.id;
211        let res = hummock_version_delta::Entity::delete_many()
212            .filter(hummock_version_delta::Column::Id.lte(version_id))
213            .exec(&self.env.meta_store_ref().conn)
214            .await?;
215        tracing::debug!(rows_affected = res.rows_affected, "Deleted version deltas");
216        versioning
217            .hummock_version_deltas
218            .retain(|id, _| *id > version_id);
219        #[cfg(test)]
220        {
221            drop(context_info);
222            drop(versioning_guard);
223            self.check_state_consistency().await;
224        }
225        Ok(res.rows_affected as usize)
226    }
227
228    /// Filters by Hummock version and Writes GC history.
229    pub async fn finalize_objects_to_delete(
230        &self,
231        object_ids: impl Iterator<Item = HummockObjectId>,
232    ) -> Result<Vec<HummockObjectId>> {
233        // This lock ensures `commit_epoch` and `report_compat_task` can see the latest GC history during sanity check.
234        let versioning = self
235            .versioning
236            .read_with_process_name("finalize_objects_to_delete")
237            .await;
238        let min_pinned_version_id = self
239            .context_info
240            .read_with_process_name("finalize_objects_to_delete")
241            .await
242            .min_pinned_version_id();
243        let mut tracked_object_ids: HashSet<HummockObjectId> =
244            versioning.get_tracked_object_ids(min_pinned_version_id);
245        tracked_object_ids.extend(self.gc_manager.change_log_retention.objects());
246        let to_delete = object_ids
247            .filter(|object_id| !tracked_object_ids.contains(object_id))
248            .collect_vec();
249        // Even an empty batch must advance the persisted GC clock and expire old history
250        // when GC history is enabled.
251        self.write_gc_history(to_delete.iter().copied()).await?;
252        Ok(to_delete)
253    }
254
255    /// LIST object store and DELETE stale objects, in batches.
256    /// GC can be very slow. Spawn a dedicated tokio task for it.
257    pub async fn start_full_gc(
258        &self,
259        object_retention_time: Duration,
260        prefix: Option<String>,
261        backup_manager: Option<BackupManagerRef>,
262    ) -> Result<()> {
263        if !self.full_gc_state.try_start() {
264            return Err(anyhow::anyhow!("failed to start GC due to an ongoing process").into());
265        }
266        let _guard = scopeguard::guard(self.full_gc_state.clone(), |full_gc_state| {
267            full_gc_state.stop()
268        });
269        self.metrics.full_gc_trigger_count.inc();
270        let object_retention_time = cmp::max(
271            object_retention_time,
272            Duration::from_secs(self.env.opts.min_sst_retention_time_sec),
273        );
274        let limit = self.env.opts.full_gc_object_limit;
275        let mut start_after = None;
276        let object_retention_watermark = self
277            .now()
278            .await?
279            .saturating_sub(object_retention_time.as_secs());
280        let mut total_object_count = 0;
281        let mut total_object_size = 0;
282        tracing::info!(
283            retention_sec = object_retention_time.as_secs(),
284            prefix,
285            limit,
286            "Start GC."
287        );
288        loop {
289            tracing::debug!(
290                retention_sec = object_retention_time.as_secs(),
291                prefix,
292                start_after,
293                limit,
294                "Start a GC batch."
295            );
296            let (object_ids, batch_object_count, batch_object_size, next_start_after) = self
297                .gc_manager
298                .list_objects(
299                    object_retention_watermark,
300                    prefix.clone(),
301                    start_after.clone(),
302                    Some(limit),
303                )
304                .await?;
305            total_object_count += batch_object_count;
306            total_object_size += batch_object_size;
307            tracing::debug!(
308                ?object_ids,
309                batch_object_count,
310                batch_object_size,
311                "Finish listing a GC batch."
312            );
313            self.complete_gc_batch(object_ids, backup_manager.clone())
314                .await?;
315            if next_start_after.is_none() {
316                break;
317            }
318            start_after = next_start_after;
319        }
320        tracing::info!(total_object_count, total_object_size, "Finish GC");
321        self.metrics.total_object_size.set(total_object_size as _);
322        self.metrics.total_object_count.set(total_object_count as _);
323        match self.time_travel_pinned_object_count().await {
324            Ok(count) => {
325                self.metrics.time_travel_object_count.set(count as _);
326            }
327            Err(err) => {
328                use thiserror_ext::AsReport;
329                tracing::warn!(error = %err.as_report(), "Failed to count time travel objects.");
330            }
331        }
332        Ok(())
333    }
334
335    /// Given candidate objects to delete, filter out false positive.
336    /// Returns number of objects to delete.
337    pub(crate) async fn complete_gc_batch(
338        &self,
339        object_ids: HashSet<HummockObjectId>,
340        backup_manager: Option<BackupManagerRef>,
341    ) -> Result<usize> {
342        if object_ids.is_empty() {
343            return Ok(0);
344        }
345        // It's crucial to collect_min_uncommitted_object_id (i.e. `min_object_id`) only after LIST object store (i.e. `object_ids`).
346        // Because after getting `min_object_id`, new compute nodes may join and generate new uncommitted objects that are not covered by `min_sst_id`.
347        // By getting `min_object_id` after `object_ids`, it's ensured `object_ids` won't include any objects from those new compute nodes.
348        let min_object_id = collect_min_uncommitted_object_id(
349            &self.metadata_manager,
350            self.env.stream_client_pool(),
351        )
352        .await?;
353        let metrics = &self.metrics;
354        let candidate_object_number = object_ids.len();
355        metrics
356            .full_gc_candidate_object_count
357            .observe(candidate_object_number as _);
358        // Filter the uncommitted-object watermark before scanning references.
359        let object_ids = object_ids
360            .into_iter()
361            .filter(|id| id.as_raw() < min_object_id)
362            .collect_vec();
363        let after_min_object_id = object_ids.len();
364        let _change_log_guard = self.gc_manager.change_log_retention.pin(true);
365        // As in minor GC, exclude version/changelog references before scanning time travel
366        // and backups. Even if those owners publish after their scans and the live references
367        // disappear, these objects cannot reenter this batch. No version safe point is needed.
368        let object_ids = {
369            let version_pinned = self.get_gc_tracked_object_ids().await;
370            object_ids
371                .into_iter()
372                .filter(|id| !version_pinned.contains(id))
373                .collect_vec()
374        };
375        let after_initial_version = object_ids.len();
376        let filter_by_time_travel_start_time = Instant::now();
377        let mut object_ids = self
378            .filter_out_objects_by_time_travel(object_ids.into_iter())
379            .await?;
380        tracing::info!(elapsed = ?filter_by_time_travel_start_time.elapsed(), "filter out objects by time travel in full GC");
381        let after_time_travel = object_ids.len();
382        // The batch pin retains changelog references even if a backup publishes after
383        // this manifest scan and releases its own pin. No backup job lock is needed.
384        if let Some(backup_manager) = &backup_manager {
385            let backup_pinned = backup_manager.list_pinned_object_ids().await;
386            object_ids.retain(|id| !backup_pinned.contains(&id.as_raw()));
387        }
388        let after_metadata_backup = object_ids.len();
389        #[cfg(test)]
390        retention_tests::pause_gc_retention_handoff(self).await;
391        // Recheck references and write history under the same versioning read lock so
392        // commits and compaction reports observe the final deletion decision. Only objects
393        // that passed time-travel and backup filtering may be recorded in GC history.
394        let after_version = self
395            .finalize_objects_to_delete(object_ids.into_iter())
396            .await?;
397        let after_version_count = after_version.len();
398        metrics
399            .full_gc_selected_object_count
400            .observe(after_version_count as _);
401        tracing::info!(
402            candidate_object_number,
403            after_min_object_id,
404            after_initial_version,
405            after_time_travel,
406            after_metadata_backup,
407            after_version_count,
408            "complete gc batch"
409        );
410        self.delete_objects(after_version).await?;
411        Ok(after_version_count)
412    }
413
414    pub async fn now(&self) -> Result<u64> {
415        let mut guard = self.now.lock().await;
416        let new_now = SystemTime::now()
417            .duration_since(SystemTime::UNIX_EPOCH)
418            .expect("Clock may have gone backwards")
419            .as_secs();
420        if new_now < *guard {
421            return Err(anyhow::anyhow!(format!(
422                "unexpected decreasing now, old={}, new={}",
423                *guard, new_now
424            ))
425            .into());
426        }
427        *guard = new_now;
428        drop(guard);
429        // Persist now to maintain non-decreasing even after a meta node reboot.
430        let m = hummock_sequence::ActiveModel {
431            name: ActiveValue::Set(HUMMOCK_NOW.into()),
432            seq: ActiveValue::Set(new_now.try_into().unwrap()),
433        };
434        hummock_sequence::Entity::insert(m)
435            .on_conflict(
436                OnConflict::column(hummock_sequence::Column::Name)
437                    .update_column(hummock_sequence::Column::Seq)
438                    .to_owned(),
439            )
440            .exec(&self.env.meta_store_ref().conn)
441            .await?;
442        Ok(new_now)
443    }
444
445    pub(crate) async fn load_now(&self) -> Result<Option<u64>> {
446        let now = hummock_sequence::Entity::find_by_id(HUMMOCK_NOW.to_owned())
447            .one(&self.env.meta_store_ref().conn)
448            .await?
449            .map(|m| m.seq.try_into().unwrap());
450        Ok(now)
451    }
452
453    async fn write_gc_history(
454        &self,
455        object_ids: impl Iterator<Item = HummockObjectId>,
456    ) -> Result<()> {
457        if self.env.opts.gc_history_retention_time_sec == 0 {
458            return Ok(());
459        }
460        let now = self.now().await?;
461        let dt = DateTime::from_timestamp(now.try_into().unwrap(), 0).unwrap();
462        let mut models = object_ids.map(|o| hummock_gc_history::ActiveModel {
463            object_id: Set(o.as_raw().into()),
464            mark_delete_at: Set(dt.naive_utc()),
465        });
466        let db = &self.meta_store_ref().conn;
467        let gc_history_low_watermark = DateTime::from_timestamp(
468            now.saturating_sub(self.env.opts.gc_history_retention_time_sec)
469                .try_into()
470                .unwrap(),
471            0,
472        )
473        .unwrap();
474        hummock_gc_history::Entity::delete_many()
475            .filter(hummock_gc_history::Column::MarkDeleteAt.lt(gc_history_low_watermark))
476            .exec(db)
477            .await?;
478        let mut is_finished = false;
479        while !is_finished {
480            let mut batch = vec![];
481            let mut count: usize = self.env.opts.hummock_gc_history_insert_batch_size;
482            while count > 0 {
483                let Some(m) = models.next() else {
484                    is_finished = true;
485                    break;
486                };
487                count -= 1;
488                batch.push(m);
489            }
490            if batch.is_empty() {
491                break;
492            }
493            hummock_gc_history::Entity::insert_many(batch)
494                .on_conflict_do_nothing()
495                .exec(db)
496                .await?;
497        }
498        Ok(())
499    }
500
501    pub async fn delete_time_travel_metadata(
502        &self,
503        pinned_snapshot_epochs: HashMap<TableId, HashSet<HummockEpoch>>,
504    ) -> MetaResult<()> {
505        let current_epoch_time = Epoch::now().physical_time();
506        let epoch_watermark = Epoch::from_physical_time(
507            current_epoch_time.saturating_sub(
508                self.env
509                    .system_params_reader()
510                    .await
511                    .time_travel_retention_ms(),
512            ),
513        )
514        .0;
515        self.truncate_time_travel_metadata(epoch_watermark, pinned_snapshot_epochs)
516            .await?;
517        Ok(())
518    }
519
520    /// Deletes stale objects from object store.
521    ///
522    /// Deduplicates within each batch of at most 1,000 input IDs. On success, returns the input
523    /// count, including duplicates; deletion errors stop subsequent batches.
524    pub async fn delete_objects(&self, objects_to_delete: Vec<HummockObjectId>) -> Result<usize> {
525        #[cfg(test)]
526        retention_tests::pause_gc_deletion(self).await;
527        let total = objects_to_delete.len();
528        for objects in objects_to_delete.chunks(1000) {
529            if self.env.opts.vacuum_spin_interval_ms != 0 {
530                tokio::time::sleep(Duration::from_millis(self.env.opts.vacuum_spin_interval_ms))
531                    .await;
532            }
533            let delete_batch: HashSet<_> = objects.iter().copied().collect();
534            tracing::info!(?delete_batch, "Attempt to delete objects.");
535            self.gc_manager
536                .delete_objects(delete_batch.iter().copied())
537                .await?;
538            tracing::debug!(deleted_object_ids = ?delete_batch, "Finish deleting objects.");
539        }
540        Ok(total)
541    }
542
543    /// Snapshot references before scanning time travel and backup manifests. The caller
544    /// must already hold a changelog retention pin and have collected its GC candidates.
545    async fn get_gc_tracked_object_ids(&self) -> HashSet<HummockObjectId> {
546        let versioning = self
547            .versioning
548            .read_with_process_name("get_gc_tracked_object_ids")
549            .await;
550        let min_pinned_version_id = self
551            .context_info
552            .read_with_process_name("get_gc_tracked_object_ids")
553            .await
554            .min_pinned_version_id();
555        let mut pinned = versioning.get_tracked_object_ids(min_pinned_version_id);
556        pinned.extend(self.gc_manager.change_log_retention.objects());
557        pinned
558    }
559
560    /// Minor GC checks queued stale objects for references before deleting them.
561    pub async fn try_start_minor_gc(&self, backup_manager: BackupManagerRef) -> Result<()> {
562        const MIN_MINOR_GC_OBJECT_COUNT: usize = 1000;
563        let Some(object_ids) = self
564            .gc_manager
565            .try_take_may_delete_object_ids(MIN_MINOR_GC_OBJECT_COUNT)
566        else {
567            return Ok(());
568        };
569        let _change_log_guard = self.gc_manager.change_log_retention.pin(true);
570        #[cfg(test)]
571        retention_tests::pause_gc_retention_handoff(self).await;
572        // The version_pinned is obtained after the candidate object_ids for deletion, which is new enough for filtering purpose.
573        let version_pinned = self.get_gc_tracked_object_ids().await;
574        let object_ids = object_ids
575            .into_iter()
576            .filter(|s| !version_pinned.contains(s));
577        let filter_by_time_travel_start_time = Instant::now();
578        let object_ids = self.filter_out_objects_by_time_travel(object_ids).await?;
579        tracing::info!(elapsed = ?filter_by_time_travel_start_time.elapsed(), "filter out objects by time travel in minor GC");
580        let backup_pinned = backup_manager.list_pinned_object_ids().await;
581        // Full GC can reclaim backup-pinned objects after the backup is deleted.
582        self.delete_objects(
583            object_ids
584                .into_iter()
585                .filter(|id| !backup_pinned.contains(&id.as_raw()))
586                .collect(),
587        )
588        .await?;
589        Ok(())
590    }
591}
592
593async fn collect_min_uncommitted_object_id(
594    metadata_manager: &MetadataManager,
595    client_pool: &StreamClientPool,
596) -> Result<HummockRawObjectId> {
597    let futures = metadata_manager
598        .list_active_streaming_compute_nodes()
599        .await
600        .map_err(|err| Error::MetaStore(err.into()))?
601        .into_iter()
602        .map(|worker_node| async move {
603            let client = client_pool.get(&worker_node).await?;
604            let request = GetMinUncommittedObjectIdRequest {};
605            client.get_min_uncommitted_object_id(request).await
606        });
607    let min_watermark = try_join_all(futures)
608        .await
609        .map_err(|err| Error::Internal(err.into()))?
610        .into_iter()
611        .map(|resp| resp.min_uncommitted_object_id)
612        .min()
613        .unwrap_or(u64::MAX.into());
614    Ok(min_watermark)
615}
616
617pub struct FullGcState {
618    is_started: AtomicBool,
619}
620
621impl FullGcState {
622    pub fn new() -> Self {
623        Self {
624            is_started: AtomicBool::new(false),
625        }
626    }
627
628    pub fn try_start(&self) -> bool {
629        self.is_started
630            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
631            .is_ok()
632    }
633
634    pub fn stop(&self) {
635        self.is_started.store(false, Ordering::SeqCst);
636    }
637}
638
639#[cfg(test)]
640mod retention_tests;
641
642#[cfg(test)]
643mod tests {
644    use std::sync::Arc;
645    use std::time::Duration;
646
647    use itertools::Itertools;
648    use risingwave_hummock_sdk::HummockObjectId;
649    use risingwave_hummock_sdk::compaction_group::StaticCompactionGroupId;
650    use risingwave_rpc_client::HummockMetaClient;
651
652    use crate::hummock::MockHummockMetaClient;
653    use crate::hummock::test_utils::{add_test_tables, setup_compute_env};
654
655    #[tokio::test]
656    async fn test_full_gc() {
657        let (_env, hummock_manager, _cluster_manager, worker_id) = setup_compute_env(80).await;
658        let hummock_meta_client: Arc<dyn HummockMetaClient> = Arc::new(MockHummockMetaClient::new(
659            hummock_manager.clone(),
660            worker_id as _,
661        ));
662        let compaction_group_id = StaticCompactionGroupId::StateDefault;
663        hummock_manager
664            .start_full_gc(
665                Duration::from_secs(hummock_manager.env.opts.min_sst_retention_time_sec + 1),
666                None,
667                None,
668            )
669            .await
670            .unwrap();
671
672        // Empty input results immediate return, without waiting heartbeat.
673        hummock_manager
674            .complete_gc_batch(vec![].into_iter().collect(), None)
675            .await
676            .unwrap();
677
678        // LSMtree is empty. All input object ids should be treated as garbage.
679        // Use fake object ids, because they'll be written to GC history and they shouldn't affect later commit.
680        assert_eq!(
681            3,
682            hummock_manager
683                .complete_gc_batch(
684                    [i64::MAX as u64 - 2, i64::MAX as u64 - 1, i64::MAX as u64]
685                        .into_iter()
686                        .map(|id| HummockObjectId::Sstable(id.into()))
687                        .collect(),
688                    None,
689                )
690                .await
691                .unwrap()
692        );
693
694        // All committed SST ids should be excluded from GC.
695        let sst_infos = add_test_tables(
696            hummock_manager.as_ref(),
697            hummock_meta_client.clone(),
698            compaction_group_id,
699        )
700        .await;
701        let committed_object_ids = sst_infos
702            .into_iter()
703            .flatten()
704            .map(|s| s.object_id)
705            .sorted()
706            .collect_vec();
707        assert!(!committed_object_ids.is_empty());
708        let max_committed_object_id = *committed_object_ids.iter().max().unwrap();
709        assert_eq!(
710            1,
711            hummock_manager
712                .complete_gc_batch(
713                    [committed_object_ids, vec![max_committed_object_id + 1]]
714                        .concat()
715                        .into_iter()
716                        .map(HummockObjectId::Sstable)
717                        .collect(),
718                    None,
719                )
720                .await
721                .unwrap()
722        );
723    }
724}