Skip to main content

cranpose_core/
snapshot_pinning.rs

1/// Snapshot pinning system to prevent premature garbage collection of state records.
2///
3/// This module implements a pinning table that tracks which snapshot IDs need to remain
4/// alive. When a snapshot is created, it "pins" the lowest snapshot ID that it depends on,
5/// preventing state records from those snapshots from being garbage collected.
6///
7/// Uses SnapshotDoubleIndexHeap for O(log N) pin/unpin and O(1) lowest queries.
8/// Based on Jetpack Compose's pinning mechanism (Snapshot.kt:714-722, 1954).
9use crate::snapshot_double_index_heap::SnapshotDoubleIndexHeap;
10use crate::snapshot_id_set::{SnapshotId, SnapshotIdSet};
11use std::cell::RefCell;
12
13/// A handle to a pinned snapshot. Dropping this handle releases the pin.
14///
15/// Internally stores a heap handle for O(log N) removal.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct PinHandle(usize);
18
19impl PinHandle {
20    /// Invalid pin handle constant (0 is reserved as invalid).
21    pub const INVALID: PinHandle = PinHandle(0);
22
23    /// Check if this handle is valid (non-zero).
24    pub fn is_valid(&self) -> bool {
25        self.0 != 0
26    }
27}
28
29/// The global pinning table that tracks pinned snapshots using a min-heap.
30struct PinningTable {
31    /// Min-heap of pinned snapshot IDs for O(1) lowest queries
32    heap: SnapshotDoubleIndexHeap,
33}
34
35impl PinningTable {
36    fn new() -> Self {
37        Self {
38            heap: SnapshotDoubleIndexHeap::new(),
39        }
40    }
41
42    /// Add a pin for the given snapshot ID, returning a handle.
43    ///
44    /// Time complexity: O(log N)
45    fn add(&mut self, snapshot_id: SnapshotId) -> PinHandle {
46        let heap_handle = self.heap.add(snapshot_id);
47        // Heap handles start at 0, but we reserve 0 as INVALID for PinHandle
48        // So we offset by 1: heap handle 0 → PinHandle(1), etc.
49        PinHandle(heap_handle + 1)
50    }
51
52    /// Remove a pin by handle.
53    ///
54    /// Time complexity: O(log N)
55    fn remove(&mut self, handle: PinHandle) -> bool {
56        if !handle.is_valid() {
57            return false;
58        }
59
60        // Convert PinHandle back to heap handle (subtract 1)
61        let heap_handle = handle.0 - 1;
62
63        // Verify handle is within bounds
64        if heap_handle < usize::MAX {
65            self.heap.remove(heap_handle);
66            true
67        } else {
68            false
69        }
70    }
71
72    /// Get the lowest pinned snapshot ID, or None if nothing is pinned.
73    ///
74    /// Time complexity: O(1)
75    fn lowest_pinned(&self) -> Option<SnapshotId> {
76        if self.heap.is_empty() {
77            None
78        } else {
79            // Use 0 as default (will never be returned since heap is non-empty)
80            Some(self.heap.lowest_or_default(0))
81        }
82    }
83
84    /// Get the count of pins (for testing/debugging).
85    fn pin_count(&self) -> usize {
86        self.heap.len()
87    }
88}
89
90thread_local! {
91    // Global pinning table protected by a mutex.
92    static PINNING_TABLE: RefCell<PinningTable> = RefCell::new(PinningTable::new());
93}
94
95/// Pin a snapshot and its invalid set, returning a handle.
96///
97/// This should be called when a snapshot is created to ensure that state records
98/// from the pinned snapshot and all its dependencies remain valid.
99///
100/// # Arguments
101/// * `snapshot_id` - The ID of the snapshot being created
102/// * `invalid` - The set of invalid snapshot IDs for this snapshot
103///
104/// # Returns
105/// A pin handle that should be released when the snapshot is disposed.
106///
107/// # Time Complexity
108/// O(log N) where N is the number of pinned snapshots
109pub fn track_pinning(snapshot_id: SnapshotId, invalid: &SnapshotIdSet) -> PinHandle {
110    // Pin the lowest snapshot ID that this snapshot depends on
111    let pinned_id = invalid.lowest(snapshot_id);
112
113    PINNING_TABLE.with(|cell| cell.borrow_mut().add(pinned_id))
114}
115
116/// Release a pinned snapshot.
117///
118/// # Arguments
119/// * `handle` - The pin handle returned by `track_pinning`
120///
121/// This must be called while holding the appropriate lock (sync).
122///
123/// # Time Complexity
124/// O(log N) where N is the number of pinned snapshots
125pub fn release_pinning(handle: PinHandle) {
126    if !handle.is_valid() {
127        return;
128    }
129
130    PINNING_TABLE.with(|cell| {
131        cell.borrow_mut().remove(handle);
132    });
133}
134
135/// Get the lowest currently pinned snapshot ID.
136///
137/// This is used to determine which state records can be safely garbage collected.
138/// Any state records from snapshots older than this ID are still potentially in use.
139///
140/// # Time Complexity
141/// O(1)
142pub fn lowest_pinned_snapshot() -> Option<SnapshotId> {
143    PINNING_TABLE.with(|cell| cell.borrow().lowest_pinned())
144}
145
146/// Get the current count of pinned snapshots (for testing).
147/// Get the current count of pinned snapshots (for testing/debugging).
148pub fn pin_count() -> usize {
149    PINNING_TABLE.with(|cell| cell.borrow().pin_count())
150}
151
152/// Reset the pinning table (for testing).
153#[cfg(test)]
154pub fn reset_pinning_table() {
155    PINNING_TABLE.with(|cell| {
156        let mut table = cell.borrow_mut();
157        table.heap = SnapshotDoubleIndexHeap::new();
158    });
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    // Helper to ensure tests start with clean state
166    fn setup() {
167        reset_pinning_table();
168    }
169
170    #[test]
171    fn test_invalid_handle() {
172        let handle = PinHandle::INVALID;
173        assert!(!handle.is_valid());
174        assert_eq!(handle.0, 0);
175    }
176
177    #[test]
178    fn test_valid_handle() {
179        setup();
180        let invalid = SnapshotIdSet::new().set(10);
181        let handle = track_pinning(20, &invalid);
182        assert!(handle.is_valid());
183        assert!(handle.0 > 0);
184    }
185
186    #[test]
187    fn test_track_and_release() {
188        setup();
189
190        let invalid = SnapshotIdSet::new().set(10);
191        let handle = track_pinning(20, &invalid);
192
193        assert_eq!(pin_count(), 1);
194        assert_eq!(lowest_pinned_snapshot(), Some(10));
195
196        release_pinning(handle);
197        assert_eq!(pin_count(), 0);
198        assert_eq!(lowest_pinned_snapshot(), None);
199    }
200
201    #[test]
202    fn test_multiple_pins() {
203        setup();
204
205        let invalid1 = SnapshotIdSet::new().set(10);
206        let handle1 = track_pinning(20, &invalid1);
207
208        let invalid2 = SnapshotIdSet::new().set(5).set(15);
209        let handle2 = track_pinning(30, &invalid2);
210
211        assert_eq!(pin_count(), 2);
212        assert_eq!(lowest_pinned_snapshot(), Some(5));
213
214        // Release first pin
215        release_pinning(handle1);
216        assert_eq!(pin_count(), 1);
217        assert_eq!(lowest_pinned_snapshot(), Some(5));
218
219        // Release second pin
220        release_pinning(handle2);
221        assert_eq!(pin_count(), 0);
222        assert_eq!(lowest_pinned_snapshot(), None);
223    }
224
225    #[test]
226    fn test_duplicate_pins() {
227        setup();
228
229        // Pin the same snapshot ID twice
230        let invalid = SnapshotIdSet::new().set(10);
231        let handle1 = track_pinning(20, &invalid);
232        let handle2 = track_pinning(25, &invalid);
233
234        assert_eq!(pin_count(), 2);
235        assert_eq!(lowest_pinned_snapshot(), Some(10));
236
237        // Releasing one doesn't unpin completely
238        release_pinning(handle1);
239        assert_eq!(pin_count(), 1);
240        assert_eq!(lowest_pinned_snapshot(), Some(10));
241
242        // Releasing second one unpins completely
243        release_pinning(handle2);
244        assert_eq!(pin_count(), 0);
245        assert_eq!(lowest_pinned_snapshot(), None);
246    }
247
248    #[test]
249    fn test_pin_ordering() {
250        setup();
251
252        // Add pins in non-sorted order
253        let invalid1 = SnapshotIdSet::new().set(30);
254        let _handle1 = track_pinning(40, &invalid1);
255
256        let invalid2 = SnapshotIdSet::new().set(10);
257        let _handle2 = track_pinning(20, &invalid2);
258
259        let invalid3 = SnapshotIdSet::new().set(20);
260        let _handle3 = track_pinning(30, &invalid3);
261
262        // Lowest should still be 10
263        assert_eq!(lowest_pinned_snapshot(), Some(10));
264    }
265
266    #[test]
267    fn test_release_invalid_handle() {
268        setup();
269
270        // Releasing an invalid handle should not crash
271        release_pinning(PinHandle::INVALID);
272        assert_eq!(pin_count(), 0);
273    }
274
275    #[test]
276    fn test_empty_invalid_set() {
277        setup();
278
279        // Empty invalid set means snapshot depends on nothing older
280        let invalid = SnapshotIdSet::new();
281        let handle = track_pinning(100, &invalid);
282
283        // Should pin snapshot 100 itself (lowest returns the upper bound if empty)
284        assert_eq!(pin_count(), 1);
285        assert_eq!(lowest_pinned_snapshot(), Some(100));
286
287        release_pinning(handle);
288    }
289
290    #[test]
291    fn test_lowest_from_invalid_set() {
292        setup();
293
294        // Create an invalid set with multiple IDs
295        let invalid = SnapshotIdSet::new().set(5).set(10).set(15).set(20);
296        let handle = track_pinning(25, &invalid);
297
298        // Should pin the lowest ID from the invalid set
299        assert_eq!(lowest_pinned_snapshot(), Some(5));
300
301        release_pinning(handle);
302    }
303
304    #[test]
305    fn test_concurrent_snapshots() {
306        setup();
307
308        // Simulate multiple concurrent snapshots
309        let handles: Vec<_> = (0..10)
310            .map(|i| {
311                let invalid = SnapshotIdSet::new().set(i * 10);
312                track_pinning(i * 10 + 5, &invalid)
313            })
314            .collect();
315
316        assert_eq!(pin_count(), 10);
317        assert_eq!(lowest_pinned_snapshot(), Some(0));
318
319        // Release all
320        for handle in handles {
321            release_pinning(handle);
322        }
323
324        assert_eq!(pin_count(), 0);
325        assert_eq!(lowest_pinned_snapshot(), None);
326    }
327
328    #[test]
329    fn test_heap_handle_based_removal() {
330        setup();
331
332        // Test that we can remove pins using just the handle, without knowing the snapshot ID
333        let invalid1 = SnapshotIdSet::new().set(42);
334        let invalid2 = SnapshotIdSet::new().set(17);
335        let invalid3 = SnapshotIdSet::new().set(99);
336
337        let h1 = track_pinning(50, &invalid1);
338        let h2 = track_pinning(25, &invalid2);
339        let h3 = track_pinning(100, &invalid3);
340
341        assert_eq!(pin_count(), 3);
342        assert_eq!(lowest_pinned_snapshot(), Some(17));
343
344        // Remove middle value using only handle
345        release_pinning(h1);
346        assert_eq!(pin_count(), 2);
347        assert_eq!(lowest_pinned_snapshot(), Some(17));
348
349        // Remove lowest using only handle
350        release_pinning(h2);
351        assert_eq!(pin_count(), 1);
352        assert_eq!(lowest_pinned_snapshot(), Some(99));
353
354        release_pinning(h3);
355        assert!(pin_count() == 0);
356    }
357}