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