risingwave_meta/hummock/manager/gc/
change_log_retention.rs1use std::collections::{BTreeMap, HashSet};
16use std::sync::Arc;
17
18use parking_lot::Mutex;
19use risingwave_hummock_sdk::HummockObjectId;
20use risingwave_hummock_sdk::change_log::TableChangeLog;
21
22#[derive(Clone, Default)]
28pub(crate) struct ChangeLogObjectRetention(Arc<Mutex<RetentionState>>);
29
30#[derive(Default)]
31struct RetentionState {
32 next_generation: u64,
34 pins: BTreeMap<u64, usize>,
35 retired: BTreeMap<u64, Vec<RetainedChangeLogBatch>>,
36}
37
38#[derive(Clone)]
41pub(crate) enum RetainedChangeLogBatch {
42 ObjectIds(Arc<HashSet<HummockObjectId>>),
43 ChangeLog(Arc<TableChangeLog>),
44}
45
46pub(crate) struct ChangeLogObjectGuard {
47 retention: ChangeLogObjectRetention,
48 generation: u64,
49}
50
51impl ChangeLogObjectRetention {
52 pub(crate) fn pin(&self, inherit: bool) -> ChangeLogObjectGuard {
55 let mut state = self.0.lock();
56 state.next_generation += 1;
57 let generation = if inherit {
58 state
59 .pins
60 .keys()
61 .next()
62 .copied()
63 .unwrap_or(state.next_generation)
64 } else {
65 state.next_generation
66 };
67 *state.pins.entry(generation).or_default() += 1;
68 ChangeLogObjectGuard {
69 retention: self.clone(),
70 generation,
71 }
72 }
73
74 pub(crate) fn retain_batch(&self, batch: RetainedChangeLogBatch) {
78 let mut state = self.0.lock();
79 if let Some(generation) = state.pins.keys().next_back().copied() {
80 state.retired.entry(generation).or_default().push(batch);
81 }
82 drop(state);
84 }
85
86 pub(crate) fn objects(&self) -> HashSet<HummockObjectId> {
87 let batches: Vec<_> = self.0.lock().retired.values().flatten().cloned().collect();
88 let mut objects = HashSet::new();
89 for batch in batches {
90 match batch {
91 RetainedChangeLogBatch::ObjectIds(ids) => objects.extend(ids.iter().copied()),
92 RetainedChangeLogBatch::ChangeLog(log) => objects.extend(log.get_object_ids()),
93 }
94 }
95 objects
96 }
97}
98
99impl Drop for ChangeLogObjectGuard {
100 fn drop(&mut self) {
101 let mut state = self.retention.0.lock();
102 let count = state.pins.get_mut(&self.generation).unwrap();
103 *count -= 1;
104 if *count == 0 {
105 state.pins.remove(&self.generation);
106 }
107 let min_generation = state.pins.keys().next().copied();
108 let mut released = Vec::new();
111 while let Some(entry) = state.retired.first_entry() {
112 if min_generation.is_some_and(|generation| generation <= *entry.key()) {
113 break;
114 }
115 released.push(entry.remove());
116 }
117 drop(state);
118 drop(released);
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use risingwave_hummock_sdk::change_log::EpochNewChangeLog;
125 use risingwave_hummock_sdk::sstable_info::SstableInfoInner;
126
127 use super::*;
128
129 #[test]
130 fn retention_survives_publication_and_overlapping_gc() {
131 let retention = ChangeLogObjectRetention::default();
132 let first = HummockObjectId::Sstable(1.into());
133 let second = HummockObjectId::Sstable(2.into());
134 retention.retain_batch(RetainedChangeLogBatch::ObjectIds(Arc::new(HashSet::from(
135 [first],
136 ))));
137 assert!(retention.objects().is_empty());
138 let backup = retention.pin(false);
139 retention.retain_batch(RetainedChangeLogBatch::ObjectIds(Arc::new(HashSet::from(
140 [first],
141 ))));
142 let full_gc = retention.pin(true);
143 let minor_gc = retention.pin(true);
144 drop(backup);
145 assert_eq!(retention.objects(), HashSet::from([first]));
146 let next_backup = retention.pin(false);
147 retention.retain_batch(RetainedChangeLogBatch::ObjectIds(Arc::new(HashSet::from(
148 [second],
149 ))));
150 drop(full_gc);
151 assert_eq!(retention.objects(), HashSet::from([first, second]));
152 drop(minor_gc);
153 assert_eq!(retention.objects(), HashSet::from([second]));
154 drop(next_backup);
155 assert!(retention.objects().is_empty());
156 }
157
158 #[test]
159 fn mixed_batches_preserve_all_references_until_last_inherited_pin_drops() {
160 let retention = ChangeLogObjectRetention::default();
161 let first = HummockObjectId::Sstable(1.into());
162 let second = HummockObjectId::Sstable(2.into());
163 let third = HummockObjectId::Sstable(3.into());
164 let ids = Arc::new(HashSet::from([first, second]));
165 let log = Arc::new(TableChangeLog::new([EpochNewChangeLog {
166 new_value: vec![
167 SstableInfoInner {
168 object_id: 3.into(),
169 ..Default::default()
170 }
171 .into(),
172 ],
173 old_value: vec![
174 SstableInfoInner {
175 object_id: 2.into(),
176 ..Default::default()
177 }
178 .into(),
179 ],
180 non_checkpoint_epochs: vec![],
181 checkpoint_epoch: 1,
182 }]));
183 let weak_ids = Arc::downgrade(&ids);
184 let weak_log = Arc::downgrade(&log);
185 let backup = retention.pin(false);
186 retention.retain_batch(RetainedChangeLogBatch::ObjectIds(ids));
188 retention.retain_batch(RetainedChangeLogBatch::ChangeLog(log));
189 let gc = retention.pin(true);
190 drop(backup);
191 assert_eq!(retention.objects(), HashSet::from([first, second, third]));
192 assert!(weak_ids.upgrade().is_some());
193 assert!(weak_log.upgrade().is_some());
194 drop(gc);
195 assert!(retention.objects().is_empty());
196 assert!(weak_ids.upgrade().is_none());
197 assert!(weak_log.upgrade().is_none());
198
199 let unpinned = Arc::new(HashSet::from([first]));
200 let weak_unpinned = Arc::downgrade(&unpinned);
201 retention.retain_batch(RetainedChangeLogBatch::ObjectIds(unpinned));
202 assert!(weak_unpinned.upgrade().is_none());
203 }
204}