Skip to main content

hermes_core/segment/
tracker.rs

1//! Segment lifecycle tracker with reference counting
2//!
3//! Provides safe segment deletion by tracking references:
4//! - Readers acquire segment snapshots (incrementing ref counts)
5//! - When snapshot is dropped, ref counts are decremented
6//! - Segments marked for deletion are only deleted when ref count reaches 0
7//!
8//! Uses a single `parking_lot::Mutex` for all state (sub-μs holds, needed for sync Drop).
9
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use parking_lot::Mutex;
14
15use crate::segment::SegmentId;
16
17/// Internal state protected by single Mutex
18struct TrackerInner {
19    ref_counts: HashMap<String, usize>,
20    pending_deletions: HashMap<String, SegmentId>,
21    /// IDs handed to a deletion callback but not finished on disk yet.
22    scheduled_deletions: HashSet<String>,
23}
24
25/// Tracks segment references and pending deletions
26pub struct SegmentTracker {
27    inner: Mutex<TrackerInner>,
28}
29
30impl SegmentTracker {
31    /// Create a new segment tracker
32    pub fn new() -> Self {
33        Self {
34            inner: Mutex::new(TrackerInner {
35                ref_counts: HashMap::new(),
36                pending_deletions: HashMap::new(),
37                scheduled_deletions: HashSet::new(),
38            }),
39        }
40    }
41
42    /// Register a new segment (called when segment is committed)
43    pub fn register(&self, segment_id: &str) {
44        let mut inner = self.inner.lock();
45        inner.ref_counts.entry(segment_id.to_string()).or_insert(0);
46    }
47
48    /// Acquire references to a set of segments (called when taking a snapshot)
49    /// Returns the segment IDs that were successfully acquired
50    pub fn acquire(&self, segment_ids: &[String]) -> Vec<String> {
51        let mut inner = self.inner.lock();
52        let mut acquired = Vec::with_capacity(segment_ids.len());
53        for id in segment_ids {
54            if inner.pending_deletions.contains_key(id) || inner.scheduled_deletions.contains(id) {
55                continue;
56            }
57            *inner.ref_counts.entry(id.clone()).or_insert(0) += 1;
58            acquired.push(id.clone());
59        }
60        acquired
61    }
62
63    /// Release references to a set of segments (called when snapshot is dropped)
64    /// Returns segment IDs that are now ready for deletion
65    pub fn release(&self, segment_ids: &[String]) -> Vec<SegmentId> {
66        let mut inner = self.inner.lock();
67        let mut ready_for_deletion = Vec::new();
68
69        for id in segment_ids {
70            if let Some(count) = inner.ref_counts.get_mut(id) {
71                *count = count.saturating_sub(1);
72
73                if *count == 0
74                    && let Some(segment_id) = inner.pending_deletions.remove(id)
75                {
76                    inner.ref_counts.remove(id);
77                    inner.scheduled_deletions.insert(id.clone());
78                    ready_for_deletion.push(segment_id);
79                }
80            }
81        }
82
83        ready_for_deletion
84    }
85
86    /// Mark segments for deletion (called after merge completes)
87    /// Segments with ref count 0 are returned immediately for deletion
88    /// Segments with refs > 0 are queued for deletion when refs are released
89    pub fn mark_for_deletion(&self, segment_ids: &[String]) -> Vec<SegmentId> {
90        let mut inner = self.inner.lock();
91        let mut ready_for_deletion = Vec::new();
92
93        for id_str in segment_ids {
94            let Some(segment_id) = SegmentId::from_hex(id_str) else {
95                continue;
96            };
97
98            // Skip segments not tracked (already deleted or never registered)
99            let Some(&ref_count) = inner.ref_counts.get(id_str) else {
100                continue;
101            };
102
103            if ref_count == 0 {
104                inner.ref_counts.remove(id_str);
105                inner.scheduled_deletions.insert(id_str.clone());
106                ready_for_deletion.push(segment_id);
107            } else {
108                inner.pending_deletions.insert(id_str.clone(), segment_id);
109            }
110        }
111
112        ready_for_deletion
113    }
114
115    /// Check if a segment is pending deletion
116    pub fn is_pending_deletion(&self, segment_id: &str) -> bool {
117        self.inner.lock().pending_deletions.contains_key(segment_id)
118    }
119
120    /// Whether files must remain protected from orphan sweeping.
121    ///
122    /// This covers both segments waiting for readers and segments whose last
123    /// reader released them but whose asynchronous filesystem deletion has
124    /// not completed yet. Moving between those states is atomic under the
125    /// tracker lock, so the sweeper cannot race the deletion callback.
126    pub fn is_deletion_protected(&self, segment_id: &str) -> bool {
127        let inner = self.inner.lock();
128        inner.pending_deletions.contains_key(segment_id)
129            || inner.scheduled_deletions.contains(segment_id)
130    }
131
132    /// Finish the scheduled-deletion lifecycle after the filesystem attempt.
133    ///
134    /// Failed deletes are also completed here so a later orphan sweep can
135    /// retry them instead of protecting the files forever.
136    pub fn complete_deletion(&self, segment_ids: &[SegmentId]) {
137        let mut inner = self.inner.lock();
138        for segment_id in segment_ids {
139            inner.scheduled_deletions.remove(&segment_id.to_hex());
140        }
141    }
142
143    /// Get the number of active references for a segment
144    pub fn ref_count(&self, segment_id: &str) -> usize {
145        self.inner
146            .lock()
147            .ref_counts
148            .get(segment_id)
149            .copied()
150            .unwrap_or(0)
151    }
152}
153
154impl Default for SegmentTracker {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160/// RAII guard that holds references to a snapshot of segments.
161/// When dropped, releases all segment references and triggers deferred deletion.
162///
163/// Not generic over Directory — the delete callback abstracts away directory access.
164pub struct SegmentSnapshot {
165    tracker: Arc<SegmentTracker>,
166    segment_ids: Vec<String>,
167    /// Callback to delete segment files when they become ready for deletion.
168    delete_fn: Option<Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>>,
169}
170
171impl SegmentSnapshot {
172    /// Create a new snapshot holding references to the given segments
173    pub fn new(tracker: Arc<SegmentTracker>, segment_ids: Vec<String>) -> Self {
174        Self {
175            tracker,
176            segment_ids,
177            delete_fn: None,
178        }
179    }
180
181    /// Create a snapshot with a deletion callback for deferred segment cleanup
182    pub fn with_delete_fn(
183        tracker: Arc<SegmentTracker>,
184        segment_ids: Vec<String>,
185        delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>,
186    ) -> Self {
187        Self {
188            tracker,
189            segment_ids,
190            delete_fn: Some(delete_fn),
191        }
192    }
193
194    /// Get the segment IDs in this snapshot
195    pub fn segment_ids(&self) -> &[String] {
196        &self.segment_ids
197    }
198
199    /// Check if this snapshot is empty
200    pub fn is_empty(&self) -> bool {
201        self.segment_ids.is_empty()
202    }
203
204    /// Get the number of segments in this snapshot
205    pub fn len(&self) -> usize {
206        self.segment_ids.len()
207    }
208}
209
210impl Drop for SegmentSnapshot {
211    fn drop(&mut self) {
212        let to_delete = self.tracker.release(&self.segment_ids);
213        if !to_delete.is_empty() {
214            if let Some(delete_fn) = &self.delete_fn {
215                log::info!(
216                    "[segment_snapshot] dropping snapshot, deleting {} deferred segments",
217                    to_delete.len()
218                );
219                delete_fn(to_delete);
220            } else {
221                log::warn!(
222                    "[segment_snapshot] {} segments ready for deletion but no delete_fn provided",
223                    to_delete.len()
224                );
225            }
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    const SEG1: &str = "00000000000000000000000000000001";
235    const SEG2: &str = "00000000000000000000000000000002";
236
237    #[test]
238    fn test_tracker_register_and_acquire() {
239        let tracker = SegmentTracker::new();
240
241        tracker.register(SEG1);
242        tracker.register(SEG2);
243
244        let acquired = tracker.acquire(&[SEG1.to_string(), SEG2.to_string()]);
245        assert_eq!(acquired.len(), 2);
246
247        assert_eq!(tracker.ref_count(SEG1), 1);
248        assert_eq!(tracker.ref_count(SEG2), 1);
249    }
250
251    #[test]
252    fn test_tracker_release() {
253        let tracker = SegmentTracker::new();
254
255        tracker.register(SEG1);
256        tracker.acquire(&[SEG1.to_string()]);
257        tracker.acquire(&[SEG1.to_string()]);
258
259        assert_eq!(tracker.ref_count(SEG1), 2);
260
261        tracker.release(&[SEG1.to_string()]);
262        assert_eq!(tracker.ref_count(SEG1), 1);
263
264        tracker.release(&[SEG1.to_string()]);
265        assert_eq!(tracker.ref_count(SEG1), 0);
266    }
267
268    #[test]
269    fn test_tracker_mark_for_deletion_no_refs() {
270        let tracker = SegmentTracker::new();
271
272        tracker.register(SEG1);
273
274        let ready = tracker.mark_for_deletion(&[SEG1.to_string()]);
275        assert_eq!(ready.len(), 1);
276        assert!(!tracker.is_pending_deletion(SEG1));
277        assert!(tracker.is_deletion_protected(SEG1));
278
279        tracker.complete_deletion(&ready);
280        assert!(!tracker.is_deletion_protected(SEG1));
281    }
282
283    #[test]
284    fn test_tracker_mark_for_deletion_with_refs() {
285        let tracker = SegmentTracker::new();
286
287        tracker.register(SEG1);
288        tracker.acquire(&[SEG1.to_string()]);
289
290        let ready = tracker.mark_for_deletion(&[SEG1.to_string()]);
291        assert!(ready.is_empty());
292        assert!(tracker.is_pending_deletion(SEG1));
293
294        let deleted = tracker.release(&[SEG1.to_string()]);
295        assert_eq!(deleted.len(), 1);
296        assert!(!tracker.is_pending_deletion(SEG1));
297        assert!(tracker.is_deletion_protected(SEG1));
298
299        tracker.complete_deletion(&deleted);
300        assert!(!tracker.is_deletion_protected(SEG1));
301    }
302
303    #[test]
304    fn test_tracker_double_mark_for_deletion() {
305        let tracker = SegmentTracker::new();
306        tracker.register(SEG1);
307
308        let ready1 = tracker.mark_for_deletion(&[SEG1.to_string()]);
309        assert_eq!(ready1.len(), 1);
310
311        // Second mark: segment already removed from ref_counts, should return empty
312        let ready2 = tracker.mark_for_deletion(&[SEG1.to_string()]);
313        assert!(ready2.is_empty());
314    }
315
316    #[test]
317    fn test_tracker_acquire_unregistered() {
318        let tracker = SegmentTracker::new();
319
320        // Acquire a segment that was never registered — or_insert(0) makes it ref_count=1
321        let acquired = tracker.acquire(&[SEG1.to_string()]);
322        assert_eq!(acquired.len(), 1);
323        assert_eq!(tracker.ref_count(SEG1), 1);
324    }
325
326    #[test]
327    fn test_tracker_release_without_acquire() {
328        let tracker = SegmentTracker::new();
329        tracker.register(SEG1);
330
331        // Release without acquire — should not panic, ref stays at 0 (saturating_sub)
332        let deleted = tracker.release(&[SEG1.to_string()]);
333        assert!(deleted.is_empty());
334        assert_eq!(tracker.ref_count(SEG1), 0);
335    }
336
337    #[test]
338    fn test_snapshot_drop_triggers_deferred_delete() {
339        use std::sync::atomic::{AtomicUsize, Ordering};
340
341        let tracker = Arc::new(SegmentTracker::new());
342        tracker.register(SEG1);
343        tracker.register(SEG2);
344
345        let delete_count = Arc::new(AtomicUsize::new(0));
346        let dc = Arc::clone(&delete_count);
347        let delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync> = Arc::new(move |ids| {
348            dc.fetch_add(ids.len(), Ordering::SeqCst);
349        });
350
351        // Take a snapshot holding refs to both segments
352        let acquired = tracker.acquire(&[SEG1.to_string(), SEG2.to_string()]);
353        let snapshot =
354            SegmentSnapshot::with_delete_fn(Arc::clone(&tracker), acquired, Arc::clone(&delete_fn));
355
356        // Mark both for deletion — should be deferred (refs > 0)
357        let ready = tracker.mark_for_deletion(&[SEG1.to_string(), SEG2.to_string()]);
358        assert!(ready.is_empty());
359        assert!(tracker.is_pending_deletion(SEG1));
360        assert!(tracker.is_pending_deletion(SEG2));
361
362        // Drop snapshot → refs go to 0 → delete_fn called
363        drop(snapshot);
364        assert_eq!(delete_count.load(Ordering::SeqCst), 2);
365        assert!(!tracker.is_pending_deletion(SEG1));
366        assert!(!tracker.is_pending_deletion(SEG2));
367        assert!(tracker.is_deletion_protected(SEG1));
368        assert!(tracker.is_deletion_protected(SEG2));
369
370        tracker.complete_deletion(&[
371            SegmentId::from_hex(SEG1).unwrap(),
372            SegmentId::from_hex(SEG2).unwrap(),
373        ]);
374        assert!(!tracker.is_deletion_protected(SEG1));
375        assert!(!tracker.is_deletion_protected(SEG2));
376    }
377}