Skip to main content

summa_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    deletions: std::collections::HashMap<String, (u32, super::DeletionMeta)>,
177    deletion_ids: Vec<String>,
178    tracker: Arc<SegmentTracker>,
179    segment_ids: Vec<String>,
180    /// The index-global ANN artifacts paired with exactly this segment set.
181    /// Keeping the pair in one snapshot lets a codebook retrain atomically
182    /// replace every segment without old readers observing the new codebook.
183    generation: Option<Arc<PublishedIndexGeneration>>,
184    /// Callback to delete segment files when they become ready for deletion.
185    delete_fn: Option<Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>>,
186}
187
188impl SegmentSnapshot {
189    /// Create a new snapshot holding references to the given segments
190    pub fn new(tracker: Arc<SegmentTracker>, segment_ids: Vec<String>) -> Self {
191        Self {
192            tracker,
193            segment_ids,
194            generation: None,
195            deletions: Default::default(),
196            deletion_ids: Vec::new(),
197            delete_fn: None,
198        }
199    }
200
201    /// Create a snapshot with a deletion callback for deferred segment cleanup
202    pub fn with_delete_fn(
203        tracker: Arc<SegmentTracker>,
204        segment_ids: Vec<String>,
205        delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>,
206    ) -> Self {
207        Self {
208            tracker,
209            segment_ids,
210            generation: None,
211            deletions: Default::default(),
212            deletion_ids: Vec::new(),
213            delete_fn: Some(delete_fn),
214        }
215    }
216
217    /// Create a native index snapshot whose segment IDs and trained vector
218    /// structures were captured under the same publication lock.
219    pub(crate) fn with_generation(
220        tracker: Arc<SegmentTracker>,
221        segment_ids: Vec<String>,
222        generation: Arc<PublishedIndexGeneration>,
223        delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync>,
224    ) -> Self {
225        Self {
226            tracker,
227            segment_ids,
228            generation: Some(generation),
229            deletions: Default::default(),
230            deletion_ids: Vec::new(),
231            delete_fn: Some(delete_fn),
232        }
233    }
234
235    pub(crate) fn with_deletions(mut self, metadata: &crate::index::IndexMetadata) -> Self {
236        self.deletions = metadata
237            .segment_metas
238            .iter()
239            .filter_map(|(id, info)| {
240                info.deletions
241                    .clone()
242                    .map(|d| (id.clone(), (info.num_docs, d)))
243            })
244            .collect();
245        let ids: Vec<_> = self.deletions.values().map(|(_, d)| d.id.clone()).collect();
246        self.deletion_ids = self.tracker.acquire(&ids);
247        self
248    }
249
250    pub(crate) fn deletions(
251        &self,
252    ) -> &std::collections::HashMap<String, (u32, super::DeletionMeta)> {
253        &self.deletions
254    }
255
256    /// Get the segment IDs in this snapshot
257    pub fn segment_ids(&self) -> &[String] {
258        &self.segment_ids
259    }
260
261    pub(crate) fn published_generation(&self) -> Option<Arc<PublishedIndexGeneration>> {
262        self.generation.clone()
263    }
264
265    /// Check if this snapshot is empty
266    pub fn is_empty(&self) -> bool {
267        self.segment_ids.is_empty()
268    }
269
270    /// Get the number of segments in this snapshot
271    pub fn len(&self) -> usize {
272        self.segment_ids.len()
273    }
274}
275
276impl Drop for SegmentSnapshot {
277    fn drop(&mut self) {
278        let mut to_delete = self.tracker.release(&self.segment_ids);
279        to_delete.extend(self.tracker.release(&self.deletion_ids));
280        if !to_delete.is_empty() {
281            if let Some(delete_fn) = &self.delete_fn {
282                log::info!(
283                    "[segment_snapshot] dropping snapshot, deleting {} deferred segments",
284                    to_delete.len()
285                );
286                delete_fn(to_delete);
287            } else {
288                log::warn!(
289                    "[segment_snapshot] {} segments ready for deletion but no delete_fn provided",
290                    to_delete.len()
291                );
292            }
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    const SEG1: &str = "00000000000000000000000000000001";
302    const SEG2: &str = "00000000000000000000000000000002";
303
304    #[test]
305    fn test_tracker_register_and_acquire() {
306        let tracker = SegmentTracker::new();
307
308        tracker.register(SEG1);
309        tracker.register(SEG2);
310
311        let acquired = tracker.acquire(&[SEG1.to_string(), SEG2.to_string()]);
312        assert_eq!(acquired.len(), 2);
313
314        assert_eq!(tracker.ref_count(SEG1), 1);
315        assert_eq!(tracker.ref_count(SEG2), 1);
316    }
317
318    #[test]
319    fn test_tracker_release() {
320        let tracker = SegmentTracker::new();
321
322        tracker.register(SEG1);
323        tracker.acquire(&[SEG1.to_string()]);
324        tracker.acquire(&[SEG1.to_string()]);
325
326        assert_eq!(tracker.ref_count(SEG1), 2);
327
328        tracker.release(&[SEG1.to_string()]);
329        assert_eq!(tracker.ref_count(SEG1), 1);
330
331        tracker.release(&[SEG1.to_string()]);
332        assert_eq!(tracker.ref_count(SEG1), 0);
333    }
334
335    #[test]
336    fn test_tracker_mark_for_deletion_no_refs() {
337        let tracker = SegmentTracker::new();
338
339        tracker.register(SEG1);
340
341        let ready = tracker.mark_for_deletion(&[SEG1.to_string()]);
342        assert_eq!(ready.len(), 1);
343        assert!(!tracker.is_pending_deletion(SEG1));
344        assert!(tracker.is_deletion_protected(SEG1));
345
346        tracker.complete_deletion(&ready);
347        assert!(!tracker.is_deletion_protected(SEG1));
348    }
349
350    #[test]
351    fn test_tracker_mark_for_deletion_with_refs() {
352        let tracker = SegmentTracker::new();
353
354        tracker.register(SEG1);
355        tracker.acquire(&[SEG1.to_string()]);
356
357        let ready = tracker.mark_for_deletion(&[SEG1.to_string()]);
358        assert!(ready.is_empty());
359        assert!(tracker.is_pending_deletion(SEG1));
360
361        let deleted = tracker.release(&[SEG1.to_string()]);
362        assert_eq!(deleted.len(), 1);
363        assert!(!tracker.is_pending_deletion(SEG1));
364        assert!(tracker.is_deletion_protected(SEG1));
365
366        tracker.complete_deletion(&deleted);
367        assert!(!tracker.is_deletion_protected(SEG1));
368    }
369
370    #[test]
371    fn test_tracker_double_mark_for_deletion() {
372        let tracker = SegmentTracker::new();
373        tracker.register(SEG1);
374
375        let ready1 = tracker.mark_for_deletion(&[SEG1.to_string()]);
376        assert_eq!(ready1.len(), 1);
377
378        // Second mark: segment already removed from ref_counts, should return empty
379        let ready2 = tracker.mark_for_deletion(&[SEG1.to_string()]);
380        assert!(ready2.is_empty());
381    }
382
383    #[test]
384    fn test_tracker_acquire_unregistered() {
385        let tracker = SegmentTracker::new();
386
387        // Acquire a segment that was never registered — or_insert(0) makes it ref_count=1
388        let acquired = tracker.acquire(&[SEG1.to_string()]);
389        assert_eq!(acquired.len(), 1);
390        assert_eq!(tracker.ref_count(SEG1), 1);
391    }
392
393    #[test]
394    fn test_tracker_release_without_acquire() {
395        let tracker = SegmentTracker::new();
396        tracker.register(SEG1);
397
398        // Release without acquire — should not panic, ref stays at 0 (saturating_sub)
399        let deleted = tracker.release(&[SEG1.to_string()]);
400        assert!(deleted.is_empty());
401        assert_eq!(tracker.ref_count(SEG1), 0);
402    }
403
404    #[test]
405    fn test_snapshot_drop_triggers_deferred_delete() {
406        use std::sync::atomic::{AtomicUsize, Ordering};
407
408        let tracker = Arc::new(SegmentTracker::new());
409        tracker.register(SEG1);
410        tracker.register(SEG2);
411
412        let delete_count = Arc::new(AtomicUsize::new(0));
413        let dc = Arc::clone(&delete_count);
414        let delete_fn: Arc<dyn Fn(Vec<SegmentId>) + Send + Sync> = Arc::new(move |ids| {
415            dc.fetch_add(ids.len(), Ordering::SeqCst);
416        });
417
418        // Take a snapshot holding refs to both segments
419        let acquired = tracker.acquire(&[SEG1.to_string(), SEG2.to_string()]);
420        let snapshot =
421            SegmentSnapshot::with_delete_fn(Arc::clone(&tracker), acquired, Arc::clone(&delete_fn));
422
423        // Mark both for deletion — should be deferred (refs > 0)
424        let ready = tracker.mark_for_deletion(&[SEG1.to_string(), SEG2.to_string()]);
425        assert!(ready.is_empty());
426        assert!(tracker.is_pending_deletion(SEG1));
427        assert!(tracker.is_pending_deletion(SEG2));
428
429        // Drop snapshot → refs go to 0 → delete_fn called
430        drop(snapshot);
431        assert_eq!(delete_count.load(Ordering::SeqCst), 2);
432        assert!(!tracker.is_pending_deletion(SEG1));
433        assert!(!tracker.is_pending_deletion(SEG2));
434        assert!(tracker.is_deletion_protected(SEG1));
435        assert!(tracker.is_deletion_protected(SEG2));
436
437        tracker.complete_deletion(&[
438            SegmentId::from_hex(SEG1).unwrap(),
439            SegmentId::from_hex(SEG2).unwrap(),
440        ]);
441        assert!(!tracker.is_deletion_protected(SEG1));
442        assert!(!tracker.is_deletion_protected(SEG2));
443    }
444}