Skip to main content

mongreldb_core/
retention.rs

1//! Global snapshot-retention registry for a multi-table `Database`.
2//!
3//! Readers register the epoch they pin via [`SnapshotRegistry::register`] and
4//! the returned [`SnapshotGuard`] deregisters on drop. Garbage collection of
5//! superseded runs, dropped tables, and recycled WAL segments is gated on
6//! [`SnapshotRegistry::min_active`]: nothing whose retire epoch is still
7//! observable by an open reader may be physically deleted.
8
9use crate::epoch::Epoch;
10use parking_lot::Mutex;
11use std::collections::BTreeMap;
12use std::collections::HashSet;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use std::time::Instant;
16
17/// Set of transaction ids that are currently spilling into `_txn/<txn_id>/`
18/// (spec §8.5, review fix #14). A large transaction registers its id before
19/// writing its pending run and holds the [`SpillGuard`] through publish; GC
20/// consults [`ActiveSpills::is_active`] and never deletes a live txn's pending
21/// dir (deleting it would lose the spill run / fail the commit).
22#[derive(Default)]
23pub struct ActiveSpills {
24    inner: Mutex<HashSet<u64>>,
25}
26
27impl ActiveSpills {
28    pub fn new() -> Self {
29        Self {
30            inner: Mutex::new(HashSet::new()),
31        }
32    }
33
34    /// Register `txn_id` as actively spilling. The id stays protected from GC
35    /// until the returned guard is dropped.
36    pub fn register(self: &Arc<Self>, txn_id: u64) -> SpillGuard {
37        self.inner.lock().insert(txn_id);
38        SpillGuard {
39            registry: Arc::clone(self),
40            txn_id,
41        }
42    }
43
44    /// Whether `txn_id`'s pending `_txn/` dir is currently in use.
45    pub fn is_active(&self, txn_id: u64) -> bool {
46        self.inner.lock().contains(&txn_id)
47    }
48
49    /// Whether no transaction is currently spilling. Used to gate WAL-segment GC.
50    pub fn is_idle(&self) -> bool {
51        self.inner.lock().is_empty()
52    }
53
54    fn release(&self, txn_id: u64) {
55        self.inner.lock().remove(&txn_id);
56    }
57}
58
59/// RAII handle that deregisters its txn id from [`ActiveSpills`] on drop.
60pub struct SpillGuard {
61    registry: Arc<ActiveSpills>,
62    txn_id: u64,
63}
64
65impl Drop for SpillGuard {
66    fn drop(&mut self) {
67        self.registry.release(self.txn_id);
68    }
69}
70
71/// Refcounted multiset of pinned reader epochs. Tracks the lowest live snapshot
72/// so the reaper can decide what is safe to reclaim.
73#[derive(Default)]
74pub struct SnapshotRegistry {
75    /// `epoch -> count` of currently-pinned reader snapshots.
76    live: Mutex<BTreeMap<u64, u64>>,
77    /// Number of prior commit epochs compaction must keep queryable. Zero
78    /// preserves the current-state-only behavior.
79    history_epochs: AtomicU64,
80    /// Earliest epoch known to have been protected since history was enabled.
81    history_start: AtomicU64,
82}
83
84impl SnapshotRegistry {
85    pub fn new() -> Self {
86        Self {
87            live: Mutex::new(BTreeMap::new()),
88            history_epochs: AtomicU64::new(0),
89            history_start: AtomicU64::new(0),
90        }
91    }
92
93    pub fn configure_history(&self, epochs: u64, start_epoch: Epoch) {
94        self.history_start.store(start_epoch.0, Ordering::Release);
95        self.history_epochs.store(epochs, Ordering::Release);
96    }
97
98    pub fn history_config(&self) -> (u64, Epoch) {
99        (
100            self.history_epochs.load(Ordering::Acquire),
101            Epoch(self.history_start.load(Ordering::Acquire)),
102        )
103    }
104
105    /// Earliest epoch guaranteed available by the rolling history window.
106    /// Returns `None` when historical retention is disabled.
107    pub fn history_floor(&self, visible: Epoch) -> Option<Epoch> {
108        let (epochs, start) = self.history_config();
109        (epochs > 0).then(|| Epoch(start.0.max(visible.0.saturating_sub(epochs))))
110    }
111
112    /// Register a pinned reader at `epoch`. The snapshot stays retained until
113    /// the returned guard is dropped.
114    pub fn register(&self, epoch: Epoch) -> SnapshotGuard<'_> {
115        let mut live = self.live.lock();
116        *live.entry(epoch.0).or_insert(0) += 1;
117        SnapshotGuard {
118            registry: self,
119            epoch,
120        }
121    }
122
123    /// The lowest currently-live pinned epoch. If no reader is active, returns
124    /// `visible` (nothing older than the reader watermark is retained, so GC is
125    /// free to reclaim anything strictly below it).
126    pub fn min_active(&self, visible: Epoch) -> Epoch {
127        match self.live.lock().keys().next().copied() {
128            Some(min) => Epoch(min),
129            None => visible,
130        }
131    }
132
133    /// The lowest currently-pinned epoch, or `None` when no reader is active.
134    /// Unlike [`Self::min_active`] this distinguishes "no readers" from "a reader
135    /// pinned at `visible`", which compaction needs: with no readers it may drop
136    /// superseded versions/tombstones freely, but a pin (even at `visible`) must
137    /// preserve the version that reader can still see.
138    pub fn min_pinned(&self) -> Option<Epoch> {
139        self.live.lock().keys().next().copied().map(Epoch)
140    }
141
142    /// Every epoch currently held by a live registry pin (Database::snapshot
143    /// readers), ascending. Used by pin-aware index rebuild so multi-pin
144    /// registry readers — not only the oldest — keep Bitmap discovery keys.
145    pub fn live_pinned_epochs(&self) -> Vec<Epoch> {
146        self.live.lock().keys().copied().map(Epoch).collect()
147    }
148
149    fn release(&self, epoch: Epoch) {
150        let mut live = self.live.lock();
151        if let Some(count) = live.get_mut(&epoch.0) {
152            *count -= 1;
153            if *count == 0 {
154                live.remove(&epoch.0);
155            }
156        }
157    }
158}
159
160/// RAII handle that deregisters its epoch from the registry on drop.
161pub struct SnapshotGuard<'r> {
162    registry: &'r SnapshotRegistry,
163    epoch: Epoch,
164}
165
166impl Drop for SnapshotGuard<'_> {
167    fn drop(&mut self) {
168        self.registry.release(self.epoch);
169    }
170}
171
172/// An owned, shareable guard (across threads / `Arc`) for snapshots that must
173/// outlive a borrow of the registry, e.g. inside an `Arc<Database>`.
174pub struct OwnedSnapshotGuard {
175    registry: Arc<SnapshotRegistry>,
176    epoch: Epoch,
177}
178
179impl OwnedSnapshotGuard {
180    /// The epoch this guard pins.
181    pub fn epoch(&self) -> Epoch {
182        self.epoch
183    }
184}
185
186impl Drop for OwnedSnapshotGuard {
187    fn drop(&mut self) {
188        self.registry.release(self.epoch);
189    }
190}
191
192impl SnapshotRegistry {
193    /// Register a pinned reader and return an owned (clonable-handle) guard
194    /// that does not borrow the registry.
195    pub fn register_owned(self: &Arc<Self>, epoch: Epoch) -> OwnedSnapshotGuard {
196        {
197            let mut live = self.live.lock();
198            *live.entry(epoch.0).or_insert(0) += 1;
199        }
200        OwnedSnapshotGuard {
201            registry: Arc::clone(self),
202            epoch,
203        }
204    }
205}
206
207/// The version-retention pin sources of spec §10.3 (S1C-004). A version may be
208/// reclaimed only when it is older than the oldest pin of **every** source.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
210pub enum PinSource {
211    /// Oldest active transaction/read snapshot (MVCC readers). Projected from
212    /// [`SnapshotRegistry`] and the table-local pin set in diagnostics.
213    TransactionSnapshot,
214    /// Configured rolling history-retention window. Projected from
215    /// [`SnapshotRegistry::history_floor`] in diagnostics.
216    HistoryRetention,
217    /// Oldest backup / point-in-time-recovery pin.
218    BackupPitr,
219    /// Oldest replication-follower requirement.
220    Replication,
221    /// Oldest cursor / immutable read-generation pin.
222    ReadGeneration,
223    /// Oldest online index build still reading historical versions.
224    OnlineIndexBuild,
225}
226
227impl PinSource {
228    /// Every source, in stable declaration order (diagnostics iteration).
229    pub const ALL: [PinSource; 6] = [
230        PinSource::TransactionSnapshot,
231        PinSource::HistoryRetention,
232        PinSource::BackupPitr,
233        PinSource::Replication,
234        PinSource::ReadGeneration,
235        PinSource::OnlineIndexBuild,
236    ];
237
238    /// Stable lowercase label for logs and diagnostics output.
239    pub fn label(self) -> &'static str {
240        match self {
241            PinSource::TransactionSnapshot => "transaction_snapshot",
242            PinSource::HistoryRetention => "history_retention",
243            PinSource::BackupPitr => "backup_pitr",
244            PinSource::Replication => "replication",
245            PinSource::ReadGeneration => "read_generation",
246            PinSource::OnlineIndexBuild => "online_index_build",
247        }
248    }
249}
250
251/// Diagnostics view of one active pin source (S1C-004): the oldest epoch it
252/// holds, when the oldest of its pins was taken, and how many pins are live.
253/// `held_since` is `None` for projected sources ([`PinSource::TransactionSnapshot`]
254/// and [`PinSource::HistoryRetention`]) whose epochs come from the
255/// [`SnapshotRegistry`] rather than from registered [`PinGuard`]s.
256#[derive(Debug, Clone)]
257pub struct PinInfo {
258    pub source: PinSource,
259    pub oldest_epoch: Epoch,
260    pub held_since: Option<Instant>,
261    pub pin_count: usize,
262}
263
264/// Every currently-active pin source, one entry per source (S1C-004
265/// diagnostics). Empty when nothing pins version reclamation.
266#[derive(Debug, Clone, Default)]
267pub struct PinsReport {
268    pub pins: Vec<PinInfo>,
269}
270
271impl PinsReport {
272    /// The oldest epoch held by any source — the version-reclamation floor.
273    pub fn oldest_epoch(&self) -> Option<Epoch> {
274        self.pins.iter().map(|pin| pin.oldest_epoch).min()
275    }
276
277    /// The entry for `source`, if that source currently holds a pin.
278    pub fn get(&self, source: PinSource) -> Option<&PinInfo> {
279        self.pins.iter().find(|pin| pin.source == source)
280    }
281
282    pub fn is_empty(&self) -> bool {
283        self.pins.is_empty()
284    }
285
286    pub fn len(&self) -> usize {
287        self.pins.len()
288    }
289
290    /// Merge a projected epoch for `source` (a floor derived from another
291    /// retention mechanism rather than a registered guard). The reported
292    /// oldest epoch only moves down; `held_since`/`pin_count` keep describing
293    /// the registered guards, if any.
294    pub fn record_projection(&mut self, source: PinSource, epoch: Epoch) {
295        match self.pins.iter_mut().find(|pin| pin.source == source) {
296            Some(info) => info.oldest_epoch = info.oldest_epoch.min(epoch),
297            None => self.pins.push(PinInfo {
298                source,
299                oldest_epoch: epoch,
300                held_since: None,
301                pin_count: 0,
302            }),
303        }
304    }
305}
306
307struct PinEntry {
308    source: PinSource,
309    epoch: Epoch,
310    held_since: Instant,
311}
312
313/// Unified registry of version-retention pins (S1C-004).
314///
315/// Every subsystem that needs historical versions to survive reclamation —
316/// backup/PITR, replication, cursors/read generations, online index builds —
317/// registers a guard here via [`PinRegistry::pin`]. GC consults
318/// [`PinRegistry::oldest_pinned`] in addition to the [`SnapshotRegistry`]
319/// (transaction snapshots) and the configured history window, and
320/// [`PinRegistry::report`] exposes every active source for diagnostics.
321/// Guards are cheap (one map insertion) and deregister on drop.
322#[derive(Default)]
323pub struct PinRegistry {
324    /// `pin id -> entry`; ids are monotonic so equal epochs stay distinguishable.
325    pins: Mutex<BTreeMap<u64, PinEntry>>,
326    next_id: AtomicU64,
327}
328
329impl PinRegistry {
330    pub fn new() -> Self {
331        Self {
332            pins: Mutex::new(BTreeMap::new()),
333            next_id: AtomicU64::new(1),
334        }
335    }
336
337    /// Register a pin of `source` at `epoch`. Versions at or below `epoch`
338    /// stay retained until the returned guard (and every clone of it) drops.
339    pub fn pin(self: &Arc<Self>, source: PinSource, epoch: Epoch) -> PinGuard {
340        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
341        self.pins.lock().insert(
342            id,
343            PinEntry {
344                source,
345                epoch,
346                held_since: Instant::now(),
347            },
348        );
349        PinGuard {
350            registry: Arc::clone(self),
351            id,
352            source,
353            epoch,
354        }
355    }
356
357    /// The oldest epoch held by any registered pin, or `None` when no pin is
358    /// active (GC is then gated only by snapshots/history).
359    pub fn oldest_pinned(&self) -> Option<Epoch> {
360        self.pins.lock().values().map(|entry| entry.epoch).min()
361    }
362
363    /// Distinct epochs currently held by any PinRegistry pin (backup/PITR,
364    /// replication, read-generation, online-index-build, …), ascending.
365    /// Compact honors these via [`crate::engine::Table::min_active_snapshot`];
366    /// pin-aware rebuild must re-index Bitmap membership for each of them.
367    pub fn live_pin_epochs(&self) -> Vec<Epoch> {
368        let mut set = std::collections::BTreeSet::new();
369        for entry in self.pins.lock().values() {
370            set.insert(entry.epoch);
371        }
372        set.into_iter().collect()
373    }
374
375    /// The oldest epoch held by pins of `source`.
376    pub fn oldest_for(&self, source: PinSource) -> Option<Epoch> {
377        self.pins
378            .lock()
379            .values()
380            .filter(|entry| entry.source == source)
381            .map(|entry| entry.epoch)
382            .min()
383    }
384
385    /// Diagnostics: one entry per source with at least one live pin.
386    pub fn report(&self) -> PinsReport {
387        let pins = self.pins.lock();
388        let mut by_source: BTreeMap<PinSource, PinInfo> = BTreeMap::new();
389        for entry in pins.values() {
390            by_source
391                .entry(entry.source)
392                .and_modify(|info| {
393                    info.oldest_epoch = info.oldest_epoch.min(entry.epoch);
394                    info.held_since = match info.held_since {
395                        Some(since) => Some(since.min(entry.held_since)),
396                        None => Some(entry.held_since),
397                    };
398                    info.pin_count += 1;
399                })
400                .or_insert(PinInfo {
401                    source: entry.source,
402                    oldest_epoch: entry.epoch,
403                    held_since: Some(entry.held_since),
404                    pin_count: 1,
405                });
406        }
407        PinsReport {
408            pins: by_source.into_values().collect(),
409        }
410    }
411
412    fn release(&self, id: u64) {
413        self.pins.lock().remove(&id);
414    }
415}
416
417/// RAII handle that deregisters its pin from the [`PinRegistry`] on drop.
418/// Not [`Clone`]: share it behind an `Arc` when several owners must keep the
419/// same pin alive (the pin releases when the last `Arc` drops).
420pub struct PinGuard {
421    registry: Arc<PinRegistry>,
422    id: u64,
423    source: PinSource,
424    epoch: Epoch,
425}
426
427impl PinGuard {
428    pub fn source(&self) -> PinSource {
429        self.source
430    }
431
432    pub fn epoch(&self) -> Epoch {
433        self.epoch
434    }
435}
436
437impl Drop for PinGuard {
438    fn drop(&mut self) {
439        self.registry.release(self.id);
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn retention_tracks_min_active_snapshot() {
449        let r = SnapshotRegistry::new();
450        assert_eq!(r.min_active(Epoch(10)), Epoch(10));
451        let g1 = r.register(Epoch(5));
452        let g2 = r.register(Epoch(8));
453        assert_eq!(r.min_active(Epoch(10)), Epoch(5));
454        drop(g1);
455        assert_eq!(r.min_active(Epoch(10)), Epoch(8));
456        drop(g2);
457        assert_eq!(r.min_active(Epoch(10)), Epoch(10));
458    }
459
460    #[test]
461    fn retention_refcounts_duplicate_epochs() {
462        let r = SnapshotRegistry::new();
463        let a = r.register(Epoch(3));
464        let b = r.register(Epoch(3));
465        assert_eq!(r.min_active(Epoch(9)), Epoch(3));
466        drop(a);
467        assert_eq!(r.min_active(Epoch(9)), Epoch(3));
468        drop(b);
469        assert_eq!(r.min_active(Epoch(9)), Epoch(9));
470    }
471
472    #[test]
473    fn pin_registry_tracks_oldest_epoch_per_source() {
474        let registry = Arc::new(PinRegistry::new());
475        assert_eq!(registry.oldest_pinned(), None);
476        let backup = registry.pin(PinSource::BackupPitr, Epoch(7));
477        let replication = registry.pin(PinSource::Replication, Epoch(4));
478        assert_eq!(registry.oldest_pinned(), Some(Epoch(4)));
479        assert_eq!(registry.oldest_for(PinSource::BackupPitr), Some(Epoch(7)));
480        assert_eq!(registry.oldest_for(PinSource::ReadGeneration), None);
481        drop(replication);
482        assert_eq!(registry.oldest_pinned(), Some(Epoch(7)));
483        drop(backup);
484        assert_eq!(registry.oldest_pinned(), None);
485    }
486
487    #[test]
488    fn pin_registry_report_lists_every_active_source_once() {
489        let registry = Arc::new(PinRegistry::new());
490        let mut guards = Vec::new();
491        for (offset, source) in PinSource::ALL.into_iter().enumerate() {
492            guards.push(registry.pin(source, Epoch(offset as u64 + 2)));
493        }
494        // A second, newer pin of one source must not duplicate its entry.
495        guards.push(registry.pin(PinSource::BackupPitr, Epoch(50)));
496
497        let report = registry.report();
498        assert_eq!(report.len(), PinSource::ALL.len());
499        for (offset, source) in PinSource::ALL.into_iter().enumerate() {
500            let info = report.get(source).expect("source listed");
501            assert_eq!(info.oldest_epoch, Epoch(offset as u64 + 2));
502            assert!(
503                info.held_since.is_some(),
504                "registered pins carry a timestamp"
505            );
506        }
507        assert_eq!(report.get(PinSource::BackupPitr).unwrap().pin_count, 2);
508        assert_eq!(report.oldest_epoch(), Some(Epoch(2)));
509
510        drop(guards);
511        assert!(registry.report().is_empty());
512    }
513
514    #[test]
515    fn pins_report_projection_only_lowers_the_floor() {
516        let registry = Arc::new(PinRegistry::new());
517        let guard = registry.pin(PinSource::ReadGeneration, Epoch(9));
518        let mut report = registry.report();
519        // A projection older than the registered pin lowers the source floor;
520        // a newer one is ignored. Projections carry no timestamp or count.
521        report.record_projection(PinSource::ReadGeneration, Epoch(4));
522        report.record_projection(PinSource::ReadGeneration, Epoch(12));
523        report.record_projection(PinSource::TransactionSnapshot, Epoch(6));
524        let info = report.get(PinSource::ReadGeneration).unwrap();
525        assert_eq!(info.oldest_epoch, Epoch(4));
526        assert_eq!(info.pin_count, 1, "projection is not a registered guard");
527        assert!(info.held_since.is_some());
528        let projected = report.get(PinSource::TransactionSnapshot).unwrap();
529        assert_eq!(projected.oldest_epoch, Epoch(6));
530        assert_eq!(projected.pin_count, 0);
531        assert!(projected.held_since.is_none());
532        assert_eq!(report.oldest_epoch(), Some(Epoch(4)));
533        drop(guard);
534    }
535}