Skip to main content

risingwave_meta/hummock/manager/gc/
change_log_retention.rs

1// Copyright 2026 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::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/// Retains removed changelog references across backup publication and GC scans.
23///
24/// This state is independent of versioning. Callers record removals before releasing
25/// their existing versioning write lock, and check retained objects under their existing
26/// GC versioning read lock. Registration and release never acquire versioning.
27#[derive(Clone, Default)]
28pub(crate) struct ChangeLogObjectRetention(Arc<Mutex<RetentionState>>);
29
30#[derive(Default)]
31struct RetentionState {
32    // Internal retention sequence, independent of RisingWave data epochs.
33    next_generation: u64,
34    pins: BTreeMap<u64, usize>,
35    retired: BTreeMap<u64, Vec<RetainedChangeLogBatch>>,
36}
37
38/// Own the removed collection without enumerating or copying its object IDs. Cloning
39/// a batch only clones its Arc, so GC can snapshot handles under the tracker mutex.
40#[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    /// Backup pins protect subsequent removals; GC additionally inherits all active pins
53    /// before scanning manifests, so publication cannot release references behind its scan.
54    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    /// Publish an immutable batch while the caller still holds the versioning write
75    /// lock that protects the removed references. Only batch bookkeeping runs under
76    /// the tracker mutex; object IDs are enumerated later by GC.
77    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        // If no pin needs the batch, release the mutex before destroying its contents.
83        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        // Drop potentially large batches outside the mutex. Full GC can reclaim these
109        // objects even if minor GC already consumed their original candidate entries.
110        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        // Both batches have the same retention generation, and one ID occurs in both.
187        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}