1use 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 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 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 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 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 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 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 !context_info.version_safe_points.is_empty() {
206 return Ok(0);
207 }
208 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 pub async fn finalize_objects_to_delete(
230 &self,
231 object_ids: impl Iterator<Item = HummockObjectId>,
232 ) -> Result<Vec<HummockObjectId>> {
233 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 self.write_gc_history(to_delete.iter().copied()).await?;
252 Ok(to_delete)
253 }
254
255 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 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 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 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 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 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 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 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 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 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 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 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 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 hummock_manager
674 .complete_gc_batch(vec![].into_iter().collect(), None)
675 .await
676 .unwrap();
677
678 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 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}