Skip to main content

cranpose_core/
snapshot_pinning.rs

1use std::cell::RefCell;
2
3use crate::{
4    snapshot_double_index_heap::{SnapshotDoubleIndexHeap, SnapshotDoubleIndexHeapDebugStats},
5    snapshot_id_set::{SnapshotId, SnapshotIdSet},
6};
7
8/// A handle to a pinned snapshot. Dropping this handle releases the pin.
9///
10/// Internally stores a heap handle for O(log N) removal.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PinHandle(usize);
13
14impl PinHandle {
15    /// Invalid pin handle constant (0 is reserved as invalid).
16    pub const INVALID: PinHandle = PinHandle(0);
17
18    /// Check if this handle is valid (non-zero).
19    pub fn is_valid(&self) -> bool {
20        self.0 != 0
21    }
22}
23
24struct PinningTable {
25    heap: SnapshotDoubleIndexHeap,
26}
27
28impl PinningTable {
29    fn new() -> Self {
30        Self {
31            heap: SnapshotDoubleIndexHeap::new(),
32        }
33    }
34
35    fn add(&mut self, snapshot_id: SnapshotId) -> PinHandle {
36        let heap_handle = self.heap.add(snapshot_id);
37        PinHandle(heap_handle + 1)
38    }
39
40    fn remove(&mut self, handle: PinHandle) -> bool {
41        if !handle.is_valid() {
42            return false;
43        }
44
45        let heap_handle = handle.0 - 1;
46
47        if heap_handle < usize::MAX {
48            self.heap.remove(heap_handle);
49            true
50        } else {
51            false
52        }
53    }
54
55    fn lowest_pinned(&self) -> Option<SnapshotId> {
56        if self.heap.is_empty() {
57            None
58        } else {
59            Some(self.heap.lowest_or_default(0))
60        }
61    }
62
63    fn pin_count(&self) -> usize {
64        self.heap.len()
65    }
66
67    fn debug_stats(&self) -> SnapshotPinningDebugStats {
68        SnapshotPinningDebugStats {
69            pin_count: self.pin_count(),
70            lowest_pinned_snapshot: self.lowest_pinned(),
71            heap: self.heap.debug_stats(),
72        }
73    }
74}
75
76thread_local! {
77    static PINNING_TABLE: RefCell<PinningTable> = RefCell::new(PinningTable::new());
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub struct SnapshotPinningDebugStats {
82    pub pin_count: usize,
83    pub lowest_pinned_snapshot: Option<SnapshotId>,
84    pub heap: SnapshotDoubleIndexHeapDebugStats,
85}
86
87/// Pin a snapshot and its invalid set, returning a handle.
88///
89/// This should be called when a snapshot is created to ensure that state records
90/// from the pinned snapshot and all its dependencies remain valid.
91///
92/// # Arguments
93/// * `snapshot_id` - The ID of the snapshot being created
94/// * `invalid` - The set of invalid snapshot IDs for this snapshot
95///
96/// # Returns
97/// A pin handle that should be released when the snapshot is disposed.
98///
99/// # Time Complexity
100/// O(log N) where N is the number of pinned snapshots
101pub fn track_pinning(snapshot_id: SnapshotId, invalid: &SnapshotIdSet) -> PinHandle {
102    let pinned_id = invalid.lowest(snapshot_id);
103
104    PINNING_TABLE.with(|cell| cell.borrow_mut().add(pinned_id))
105}
106
107/// Release a pinned snapshot.
108///
109/// # Arguments
110/// * `handle` - The pin handle returned by `track_pinning`
111///
112/// This must be called while holding the appropriate lock (sync).
113///
114/// # Time Complexity
115/// O(log N) where N is the number of pinned snapshots
116pub fn release_pinning(handle: PinHandle) {
117    if !handle.is_valid() {
118        return;
119    }
120
121    PINNING_TABLE.with(|cell| {
122        cell.borrow_mut().remove(handle);
123    });
124}
125
126/// Get the lowest currently pinned snapshot ID.
127///
128/// This is used to determine which state records can be safely garbage collected.
129/// Any state records from snapshots older than this ID are still potentially in use.
130///
131/// # Time Complexity
132/// O(1)
133pub fn lowest_pinned_snapshot() -> Option<SnapshotId> {
134    PINNING_TABLE.with(|cell| cell.borrow().lowest_pinned())
135}
136
137/// Get the current count of pinned snapshots (for testing).
138/// Get the current count of pinned snapshots (for testing/debugging).
139pub fn pin_count() -> usize {
140    PINNING_TABLE.with(|cell| cell.borrow().pin_count())
141}
142
143pub fn debug_snapshot_pinning_stats() -> SnapshotPinningDebugStats {
144    PINNING_TABLE.with(|cell| cell.borrow().debug_stats())
145}
146
147#[cfg(test)]
148pub fn reset_pinning_table() {
149    PINNING_TABLE.with(|cell| {
150        let mut table = cell.borrow_mut();
151        table.heap = SnapshotDoubleIndexHeap::new();
152    });
153}
154
155#[cfg(test)]
156#[path = "tests/snapshot_pinning_tests.rs"]
157mod tests;