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::Arc;
14
15/// Set of transaction ids that are currently spilling into `_txn/<txn_id>/`
16/// (spec ยง8.5, review fix #14). A large transaction registers its id before
17/// writing its pending run and holds the [`SpillGuard`] through publish; GC
18/// consults [`ActiveSpills::is_active`] and never deletes a live txn's pending
19/// dir (deleting it would lose the spill run / fail the commit).
20#[derive(Default)]
21pub struct ActiveSpills {
22    inner: Mutex<HashSet<u64>>,
23}
24
25impl ActiveSpills {
26    pub fn new() -> Self {
27        Self {
28            inner: Mutex::new(HashSet::new()),
29        }
30    }
31
32    /// Register `txn_id` as actively spilling. The id stays protected from GC
33    /// until the returned guard is dropped.
34    pub fn register(self: &Arc<Self>, txn_id: u64) -> SpillGuard {
35        self.inner.lock().insert(txn_id);
36        SpillGuard {
37            registry: Arc::clone(self),
38            txn_id,
39        }
40    }
41
42    /// Whether `txn_id`'s pending `_txn/` dir is currently in use.
43    pub fn is_active(&self, txn_id: u64) -> bool {
44        self.inner.lock().contains(&txn_id)
45    }
46
47    /// Whether no transaction is currently spilling. Used to gate WAL-segment GC.
48    pub fn is_idle(&self) -> bool {
49        self.inner.lock().is_empty()
50    }
51
52    fn release(&self, txn_id: u64) {
53        self.inner.lock().remove(&txn_id);
54    }
55}
56
57/// RAII handle that deregisters its txn id from [`ActiveSpills`] on drop.
58pub struct SpillGuard {
59    registry: Arc<ActiveSpills>,
60    txn_id: u64,
61}
62
63impl Drop for SpillGuard {
64    fn drop(&mut self) {
65        self.registry.release(self.txn_id);
66    }
67}
68
69/// Refcounted multiset of pinned reader epochs. Tracks the lowest live snapshot
70/// so the reaper can decide what is safe to reclaim.
71#[derive(Default)]
72pub struct SnapshotRegistry {
73    /// `epoch -> count` of currently-pinned reader snapshots.
74    live: Mutex<BTreeMap<u64, u64>>,
75}
76
77impl SnapshotRegistry {
78    pub fn new() -> Self {
79        Self {
80            live: Mutex::new(BTreeMap::new()),
81        }
82    }
83
84    /// Register a pinned reader at `epoch`. The snapshot stays retained until
85    /// the returned guard is dropped.
86    pub fn register(&self, epoch: Epoch) -> SnapshotGuard<'_> {
87        let mut live = self.live.lock();
88        *live.entry(epoch.0).or_insert(0) += 1;
89        SnapshotGuard {
90            registry: self,
91            epoch,
92        }
93    }
94
95    /// The lowest currently-live pinned epoch. If no reader is active, returns
96    /// `visible` (nothing older than the reader watermark is retained, so GC is
97    /// free to reclaim anything strictly below it).
98    pub fn min_active(&self, visible: Epoch) -> Epoch {
99        match self.live.lock().keys().next().copied() {
100            Some(min) => Epoch(min),
101            None => visible,
102        }
103    }
104
105    /// The lowest currently-pinned epoch, or `None` when no reader is active.
106    /// Unlike [`Self::min_active`] this distinguishes "no readers" from "a reader
107    /// pinned at `visible`", which compaction needs: with no readers it may drop
108    /// superseded versions/tombstones freely, but a pin (even at `visible`) must
109    /// preserve the version that reader can still see.
110    pub fn min_pinned(&self) -> Option<Epoch> {
111        self.live.lock().keys().next().copied().map(Epoch)
112    }
113
114    fn release(&self, epoch: Epoch) {
115        let mut live = self.live.lock();
116        if let Some(count) = live.get_mut(&epoch.0) {
117            *count -= 1;
118            if *count == 0 {
119                live.remove(&epoch.0);
120            }
121        }
122    }
123}
124
125/// RAII handle that deregisters its epoch from the registry on drop.
126pub struct SnapshotGuard<'r> {
127    registry: &'r SnapshotRegistry,
128    epoch: Epoch,
129}
130
131impl Drop for SnapshotGuard<'_> {
132    fn drop(&mut self) {
133        self.registry.release(self.epoch);
134    }
135}
136
137/// An owned, shareable guard (across threads / `Arc`) for snapshots that must
138/// outlive a borrow of the registry, e.g. inside an `Arc<Database>`.
139pub struct OwnedSnapshotGuard {
140    registry: Arc<SnapshotRegistry>,
141    epoch: Epoch,
142}
143
144impl OwnedSnapshotGuard {
145    /// The epoch this guard pins.
146    pub fn epoch(&self) -> Epoch {
147        self.epoch
148    }
149}
150
151impl Drop for OwnedSnapshotGuard {
152    fn drop(&mut self) {
153        self.registry.release(self.epoch);
154    }
155}
156
157impl SnapshotRegistry {
158    /// Register a pinned reader and return an owned (clonable-handle) guard
159    /// that does not borrow the registry.
160    pub fn register_owned(self: &Arc<Self>, epoch: Epoch) -> OwnedSnapshotGuard {
161        {
162            let mut live = self.live.lock();
163            *live.entry(epoch.0).or_insert(0) += 1;
164        }
165        OwnedSnapshotGuard {
166            registry: Arc::clone(self),
167            epoch,
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn retention_tracks_min_active_snapshot() {
178        let r = SnapshotRegistry::new();
179        assert_eq!(r.min_active(Epoch(10)), Epoch(10));
180        let g1 = r.register(Epoch(5));
181        let g2 = r.register(Epoch(8));
182        assert_eq!(r.min_active(Epoch(10)), Epoch(5));
183        drop(g1);
184        assert_eq!(r.min_active(Epoch(10)), Epoch(8));
185        drop(g2);
186        assert_eq!(r.min_active(Epoch(10)), Epoch(10));
187    }
188
189    #[test]
190    fn retention_refcounts_duplicate_epochs() {
191        let r = SnapshotRegistry::new();
192        let a = r.register(Epoch(3));
193        let b = r.register(Epoch(3));
194        assert_eq!(r.min_active(Epoch(9)), Epoch(3));
195        drop(a);
196        assert_eq!(r.min_active(Epoch(9)), Epoch(3));
197        drop(b);
198        assert_eq!(r.min_active(Epoch(9)), Epoch(9));
199    }
200}