Skip to main content

sochdb_core/
reclamation.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Unified Memory Reclamation - Hazard Pointers + Epoch Hybrid
19//!
20//! This module provides a unified memory reclamation strategy combining:
21//! - **Hazard Pointers**: For hot-path reads with minimal latency overhead
22//! - **Epoch-Based Reclamation (EBR)**: For batch operations and writes
23//!
24//! # Design
25//!
26//! ```text
27//! ┌─────────────────────────────────────────────────────────────────┐
28//! │                    Unified Reclamation                          │
29//! │                                                                 │
30//! │  ┌──────────────────┐      ┌──────────────────────────────┐    │
31//! │  │  Hazard Pointers │      │     Epoch-Based GC           │    │
32//! │  │                  │      │                              │    │
33//! │  │  • Per-thread HP │      │  • Global epoch counter      │    │
34//! │  │  • Protect reads │      │  • Per-thread local epoch    │    │
35//! │  │  • O(1) protect  │      │  • Limbo list per epoch      │    │
36//! │  │  • Scan on free  │      │  • Amortized O(1) reclaim    │    │
37//! │  └──────────────────┘      └──────────────────────────────┘    │
38//! │            │                            │                       │
39//! │            └──────────┬─────────────────┘                       │
40//! │                       ▼                                         │
41//! │              ┌────────────────┐                                 │
42//! │              │ Unified Guard  │                                 │
43//! │              │                │                                 │
44//! │              │ Picks strategy │                                 │
45//! │              │ based on:      │                                 │
46//! │              │ • Contention   │                                 │
47//! │              │ • Read/Write   │                                 │
48//! │              │ • Batch size   │                                 │
49//! │              └────────────────┘                                 │
50//! └─────────────────────────────────────────────────────────────────┘
51//! ```
52//!
53//! # Usage
54//!
55//! ```rust,ignore
56//! let reclaimer = UnifiedReclaimer::new();
57//!
58//! // Hot-path reads use hazard pointers
59//! let guard = reclaimer.pin_read();
60//! let data = guard.protect(&shared_ptr);
61//! // ... use data
62//! drop(guard); // Automatically unpins
63//!
64//! // Batch operations use epochs
65//! let guard = reclaimer.pin_epoch();
66//! for item in batch {
67//!     // ... process items
68//! }
69//! reclaimer.retire(old_data);
70//! drop(guard); // Triggers epoch advancement
71//! ```
72
73use parking_lot::{Mutex, RwLock};
74use std::collections::VecDeque;
75use std::sync::Arc;
76use std::sync::atomic::{AtomicPtr, AtomicU64, AtomicUsize, Ordering};
77
78/// Maximum number of hazard pointers per thread
79const MAX_HAZARD_POINTERS: usize = 4;
80
81/// Number of epochs to keep before reclamation
82const EPOCH_GRACE_PERIODS: u64 = 2;
83
84/// Threshold for triggering reclamation scan
85const RECLAIM_THRESHOLD: usize = 64;
86
87/// Hazard pointer slot
88///
89/// Padded to 64 bytes (one cache line) to prevent false sharing between
90/// threads that call `protect()` and `release()` concurrently. Without
91/// padding, 4 slots share a cache line (each is only 16 bytes), causing
92/// MESI invalidation storms under high-frequency protect/release cycles
93/// (e.g., HNSW search with ef_search=200 → ~3200 protect/release cycles
94/// per query). Matches the cache-line alignment used for `EpochSlot`.
95#[derive(Debug)]
96#[repr(C, align(64))]
97struct HazardSlot {
98    /// The protected pointer (null if unused)
99    ptr: AtomicPtr<()>,
100    /// Thread ID that owns this slot
101    owner: AtomicU64,
102    // Remaining 48 bytes are padding to fill the 64-byte cache line
103}
104
105impl HazardSlot {
106    fn new() -> Self {
107        Self {
108            ptr: AtomicPtr::new(std::ptr::null_mut()),
109            owner: AtomicU64::new(0),
110        }
111    }
112
113    fn acquire(&self, thread_id: u64) -> bool {
114        self.owner
115            .compare_exchange(0, thread_id, Ordering::AcqRel, Ordering::Relaxed)
116            .is_ok()
117    }
118
119    fn release(&self) {
120        self.ptr.store(std::ptr::null_mut(), Ordering::Release);
121        self.owner.store(0, Ordering::Release);
122    }
123
124    fn protect(&self, ptr: *mut ()) {
125        self.ptr.store(ptr, Ordering::Release);
126    }
127
128    fn is_protecting(&self, ptr: *mut ()) -> bool {
129        self.ptr.load(Ordering::Acquire) == ptr
130    }
131}
132
133/// Hazard pointer domain for a set of threads
134pub struct HazardDomain {
135    /// Global list of hazard pointer slots
136    slots: Vec<HazardSlot>,
137    /// Number of active slots
138    active_count: AtomicUsize,
139    /// Thread-local slot indices (using thread_local! is preferred, this is fallback)
140    slot_registry: Mutex<Vec<(u64, usize)>>,
141}
142
143impl HazardDomain {
144    /// Create new hazard domain with given capacity
145    pub fn new(max_threads: usize) -> Self {
146        let capacity = max_threads * MAX_HAZARD_POINTERS;
147        let mut slots = Vec::with_capacity(capacity);
148        for _ in 0..capacity {
149            slots.push(HazardSlot::new());
150        }
151
152        Self {
153            slots,
154            active_count: AtomicUsize::new(0),
155            slot_registry: Mutex::new(Vec::new()),
156        }
157    }
158
159    /// Acquire a hazard pointer slot for the current thread
160    fn acquire_slot(&self, thread_id: u64) -> Option<usize> {
161        // First check registry for existing slot
162        {
163            let registry = self.slot_registry.lock();
164            for &(tid, idx) in registry.iter() {
165                if tid == thread_id {
166                    return Some(idx);
167                }
168            }
169        }
170
171        // Try to acquire a new slot
172        for (idx, slot) in self.slots.iter().enumerate() {
173            if slot.acquire(thread_id) {
174                let mut registry = self.slot_registry.lock();
175                registry.push((thread_id, idx));
176                self.active_count.fetch_add(1, Ordering::Relaxed);
177                return Some(idx);
178            }
179        }
180
181        None
182    }
183
184    /// Release a hazard pointer slot
185    fn release_slot(&self, thread_id: u64, slot_idx: usize) {
186        if slot_idx < self.slots.len() {
187            self.slots[slot_idx].release();
188        }
189
190        let mut registry = self.slot_registry.lock();
191        registry.retain(|&(tid, _)| tid != thread_id);
192        self.active_count.fetch_sub(1, Ordering::Relaxed);
193    }
194
195    /// Protect a pointer using hazard pointer at given slot
196    fn protect(&self, slot_idx: usize, ptr: *mut ()) {
197        if slot_idx < self.slots.len() {
198            self.slots[slot_idx].protect(ptr);
199        }
200    }
201
202    /// Check if any hazard pointer is protecting the given pointer
203    fn is_protected(&self, ptr: *mut ()) -> bool {
204        for slot in &self.slots {
205            if slot.is_protecting(ptr) {
206                return true;
207            }
208        }
209        false
210    }
211
212    /// Get current active count
213    pub fn active_count(&self) -> usize {
214        self.active_count.load(Ordering::Relaxed)
215    }
216}
217
218impl Default for HazardDomain {
219    fn default() -> Self {
220        Self::new(64) // Default to 64 threads
221    }
222}
223
224/// Epoch-based reclamation domain
225pub struct EpochDomain {
226    /// Global epoch counter
227    global_epoch: AtomicU64,
228    /// Per-thread local epochs
229    local_epochs: RwLock<Vec<AtomicU64>>,
230    /// Limbo lists per epoch (objects waiting to be freed)
231    limbo: Mutex<VecDeque<(u64, Vec<RetiredObject>)>>,
232    /// Count of retired objects pending reclamation
233    retired_count: AtomicUsize,
234}
235
236/// A retired object waiting for safe reclamation
237#[allow(dead_code)]
238struct RetiredObject {
239    ptr: *mut (),
240    destructor: fn(*mut ()),
241    size: usize,
242}
243
244// Safety: RetiredObject contains raw pointers but they're only dereferenced
245// in a single-threaded context during reclamation
246unsafe impl Send for RetiredObject {}
247
248impl EpochDomain {
249    /// Create new epoch domain
250    pub fn new() -> Self {
251        Self {
252            global_epoch: AtomicU64::new(0),
253            local_epochs: RwLock::new(Vec::new()),
254            limbo: Mutex::new(VecDeque::new()),
255            retired_count: AtomicUsize::new(0),
256        }
257    }
258
259    /// Register a thread and return its index
260    pub fn register_thread(&self) -> usize {
261        let mut epochs = self.local_epochs.write();
262        let idx = epochs.len();
263        epochs.push(AtomicU64::new(u64::MAX)); // MAX = not pinned
264        idx
265    }
266
267    /// Pin the current epoch for a thread
268    pub fn pin(&self, thread_idx: usize) {
269        let current = self.global_epoch.load(Ordering::SeqCst);
270        let epochs = self.local_epochs.read();
271        if thread_idx < epochs.len() {
272            epochs[thread_idx].store(current, Ordering::SeqCst);
273        }
274    }
275
276    /// Unpin (exit) the current epoch for a thread
277    pub fn unpin(&self, thread_idx: usize) {
278        let epochs = self.local_epochs.read();
279        if thread_idx < epochs.len() {
280            epochs[thread_idx].store(u64::MAX, Ordering::SeqCst);
281        }
282    }
283
284    /// Retire an object for later reclamation
285    pub fn retire(&self, ptr: *mut (), destructor: fn(*mut ()), size: usize) {
286        let current_epoch = self.global_epoch.load(Ordering::SeqCst);
287        let obj = RetiredObject {
288            ptr,
289            destructor,
290            size,
291        };
292
293        let mut limbo = self.limbo.lock();
294
295        // Find or create bucket for current epoch
296        if limbo.back().is_none_or(|(e, _)| *e != current_epoch) {
297            limbo.push_back((current_epoch, Vec::new()));
298        }
299
300        if let Some((_, objects)) = limbo.back_mut() {
301            objects.push(obj);
302        }
303
304        let count = self.retired_count.fetch_add(1, Ordering::Relaxed);
305
306        // Trigger reclamation if threshold exceeded
307        if count >= RECLAIM_THRESHOLD {
308            drop(limbo);
309            self.try_reclaim();
310        }
311    }
312
313    /// Advance the global epoch
314    pub fn advance_epoch(&self) {
315        self.global_epoch.fetch_add(1, Ordering::SeqCst);
316    }
317
318    /// Get minimum epoch that is safe (all threads have advanced past)
319    fn safe_epoch(&self) -> u64 {
320        let epochs = self.local_epochs.read();
321        let mut min = self.global_epoch.load(Ordering::SeqCst);
322
323        for epoch in epochs.iter() {
324            let e = epoch.load(Ordering::SeqCst);
325            if e != u64::MAX && e < min {
326                min = e;
327            }
328        }
329
330        // Safe epoch is grace periods before minimum
331        min.saturating_sub(EPOCH_GRACE_PERIODS)
332    }
333
334    /// Try to reclaim objects from old epochs
335    pub fn try_reclaim(&self) -> usize {
336        let safe = self.safe_epoch();
337        let mut reclaimed = 0;
338
339        let mut limbo = self.limbo.lock();
340
341        while let Some((epoch, _)) = limbo.front() {
342            if *epoch > safe {
343                break;
344            }
345
346            if let Some((_, objects)) = limbo.pop_front() {
347                for obj in objects {
348                    // Call destructor
349                    (obj.destructor)(obj.ptr);
350                    reclaimed += 1;
351                    self.retired_count.fetch_sub(1, Ordering::Relaxed);
352                }
353            }
354        }
355
356        reclaimed
357    }
358
359    /// Get current global epoch
360    pub fn current_epoch(&self) -> u64 {
361        self.global_epoch.load(Ordering::SeqCst)
362    }
363
364    /// Get count of pending retired objects
365    pub fn pending_count(&self) -> usize {
366        self.retired_count.load(Ordering::Relaxed)
367    }
368}
369
370impl Default for EpochDomain {
371    fn default() -> Self {
372        Self::new()
373    }
374}
375
376/// Guard for hazard pointer protection
377pub struct HazardGuard<'a> {
378    domain: &'a HazardDomain,
379    slot_idx: usize,
380    thread_id: u64,
381}
382
383impl<'a> HazardGuard<'a> {
384    /// Protect a raw pointer
385    pub fn protect(&self, ptr: *mut ()) {
386        self.domain.protect(self.slot_idx, ptr);
387    }
388
389    /// Protect a typed pointer
390    pub fn protect_typed<T>(&self, ptr: *mut T) {
391        self.protect(ptr as *mut ());
392    }
393}
394
395impl<'a> Drop for HazardGuard<'a> {
396    fn drop(&mut self) {
397        self.domain.release_slot(self.thread_id, self.slot_idx);
398    }
399}
400
401/// Guard for epoch-based protection
402pub struct EpochGuard<'a> {
403    domain: &'a EpochDomain,
404    thread_idx: usize,
405}
406
407impl<'a> Drop for EpochGuard<'a> {
408    fn drop(&mut self) {
409        self.domain.unpin(self.thread_idx);
410    }
411}
412
413/// Reclamation strategy selection
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum ReclaimStrategy {
416    /// Use hazard pointers (best for hot-path reads)
417    HazardPointer,
418    /// Use epoch-based reclamation (best for batch operations)
419    Epoch,
420    /// Automatically select based on heuristics
421    Auto,
422}
423
424/// Unified memory reclamation combining hazard pointers and epochs
425pub struct UnifiedReclaimer {
426    hazard: Arc<HazardDomain>,
427    epoch: Arc<EpochDomain>,
428    /// Thread-local epoch indices
429    thread_epochs: Mutex<std::collections::HashMap<u64, usize>>,
430    /// Strategy selection
431    default_strategy: ReclaimStrategy,
432    /// Statistics
433    stats: ReclaimStats,
434}
435
436/// Reclamation statistics
437#[derive(Debug, Default)]
438pub struct ReclaimStats {
439    pub hazard_pins: AtomicU64,
440    pub epoch_pins: AtomicU64,
441    pub objects_retired: AtomicU64,
442    pub objects_reclaimed: AtomicU64,
443    pub reclaim_cycles: AtomicU64,
444}
445
446impl ReclaimStats {
447    fn record_hazard_pin(&self) {
448        self.hazard_pins.fetch_add(1, Ordering::Relaxed);
449    }
450
451    fn record_epoch_pin(&self) {
452        self.epoch_pins.fetch_add(1, Ordering::Relaxed);
453    }
454
455    fn record_retire(&self) {
456        self.objects_retired.fetch_add(1, Ordering::Relaxed);
457    }
458
459    fn record_reclaim(&self, count: usize) {
460        self.objects_reclaimed
461            .fetch_add(count as u64, Ordering::Relaxed);
462        self.reclaim_cycles.fetch_add(1, Ordering::Relaxed);
463    }
464
465    pub fn snapshot(&self) -> ReclaimStatsSnapshot {
466        ReclaimStatsSnapshot {
467            hazard_pins: self.hazard_pins.load(Ordering::Relaxed),
468            epoch_pins: self.epoch_pins.load(Ordering::Relaxed),
469            objects_retired: self.objects_retired.load(Ordering::Relaxed),
470            objects_reclaimed: self.objects_reclaimed.load(Ordering::Relaxed),
471            reclaim_cycles: self.reclaim_cycles.load(Ordering::Relaxed),
472        }
473    }
474}
475
476#[derive(Debug, Clone)]
477pub struct ReclaimStatsSnapshot {
478    pub hazard_pins: u64,
479    pub epoch_pins: u64,
480    pub objects_retired: u64,
481    pub objects_reclaimed: u64,
482    pub reclaim_cycles: u64,
483}
484
485impl UnifiedReclaimer {
486    /// Create new unified reclaimer
487    pub fn new() -> Self {
488        Self {
489            hazard: Arc::new(HazardDomain::default()),
490            epoch: Arc::new(EpochDomain::default()),
491            thread_epochs: Mutex::new(std::collections::HashMap::new()),
492            default_strategy: ReclaimStrategy::Auto,
493            stats: ReclaimStats::default(),
494        }
495    }
496
497    /// Create with specific max thread capacity
498    pub fn with_capacity(max_threads: usize) -> Self {
499        Self {
500            hazard: Arc::new(HazardDomain::new(max_threads)),
501            epoch: Arc::new(EpochDomain::default()),
502            thread_epochs: Mutex::new(std::collections::HashMap::new()),
503            default_strategy: ReclaimStrategy::Auto,
504            stats: ReclaimStats::default(),
505        }
506    }
507
508    /// Set default reclamation strategy
509    pub fn with_strategy(mut self, strategy: ReclaimStrategy) -> Self {
510        self.default_strategy = strategy;
511        self
512    }
513
514    /// Pin for read access using hazard pointers
515    pub fn pin_hazard(&self) -> Option<HazardGuard<'_>> {
516        let thread_id = self.current_thread_id();
517        let slot_idx = self.hazard.acquire_slot(thread_id)?;
518
519        self.stats.record_hazard_pin();
520
521        Some(HazardGuard {
522            domain: &self.hazard,
523            slot_idx,
524            thread_id,
525        })
526    }
527
528    /// Pin for epoch-based access
529    pub fn pin_epoch(&self) -> EpochGuard<'_> {
530        let thread_id = self.current_thread_id();
531
532        let thread_idx = {
533            let mut epochs = self.thread_epochs.lock();
534            *epochs
535                .entry(thread_id)
536                .or_insert_with(|| self.epoch.register_thread())
537        };
538
539        self.epoch.pin(thread_idx);
540        self.stats.record_epoch_pin();
541
542        EpochGuard {
543            domain: &self.epoch,
544            thread_idx,
545        }
546    }
547
548    /// Retire an object for later reclamation
549    ///
550    /// # Safety
551    /// The pointer must have been allocated and no references should exist
552    /// outside of protected guards.
553    pub unsafe fn retire<T>(&self, ptr: *mut T) {
554        let destructor = |p: *mut ()| {
555            // Safety: caller guarantees ptr was allocated as T
556            unsafe { drop(Box::from_raw(p as *mut T)) };
557        };
558
559        self.epoch
560            .retire(ptr as *mut (), destructor, std::mem::size_of::<T>());
561        self.stats.record_retire();
562    }
563
564    /// Retire with custom destructor
565    pub fn retire_with_destructor(&self, ptr: *mut (), destructor: fn(*mut ()), size: usize) {
566        self.epoch.retire(ptr, destructor, size);
567        self.stats.record_retire();
568    }
569
570    /// Check if a pointer is protected by any hazard pointer
571    pub fn is_protected(&self, ptr: *mut ()) -> bool {
572        self.hazard.is_protected(ptr)
573    }
574
575    /// Manually trigger reclamation
576    pub fn try_reclaim(&self) -> usize {
577        let reclaimed = self.epoch.try_reclaim();
578        if reclaimed > 0 {
579            self.stats.record_reclaim(reclaimed);
580        }
581        reclaimed
582    }
583
584    /// Advance the epoch
585    pub fn advance_epoch(&self) {
586        self.epoch.advance_epoch();
587    }
588
589    /// Get current epoch
590    pub fn current_epoch(&self) -> u64 {
591        self.epoch.current_epoch()
592    }
593
594    /// Get count of objects pending reclamation
595    pub fn pending_count(&self) -> usize {
596        self.epoch.pending_count()
597    }
598
599    /// Get statistics
600    pub fn stats(&self) -> ReclaimStatsSnapshot {
601        self.stats.snapshot()
602    }
603
604    /// Get thread ID (platform-specific)
605    fn current_thread_id(&self) -> u64 {
606        // Use hash of thread ID as stable u64 identifier
607        use std::hash::{Hash, Hasher};
608        let mut hasher = std::collections::hash_map::DefaultHasher::new();
609        std::thread::current().id().hash(&mut hasher);
610        hasher.finish()
611    }
612}
613
614impl Default for UnifiedReclaimer {
615    fn default() -> Self {
616        Self::new()
617    }
618}
619
620/// Thread-local handle for efficient access
621pub struct ThreadLocalReclaimer {
622    reclaimer: Arc<UnifiedReclaimer>,
623    hazard_slot: Option<usize>,
624    epoch_idx: usize,
625    thread_id: u64,
626}
627
628impl ThreadLocalReclaimer {
629    /// Create thread-local handle
630    pub fn new(reclaimer: Arc<UnifiedReclaimer>) -> Self {
631        // Use hash of thread ID as stable u64 identifier
632        use std::hash::{Hash, Hasher};
633        let mut hasher = std::collections::hash_map::DefaultHasher::new();
634        std::thread::current().id().hash(&mut hasher);
635        let thread_id = hasher.finish();
636
637        let epoch_idx = reclaimer.epoch.register_thread();
638
639        Self {
640            reclaimer,
641            hazard_slot: None,
642            epoch_idx,
643            thread_id,
644        }
645    }
646
647    /// Pin using hazard pointer (fast path)
648    pub fn pin_hazard(&mut self) -> bool {
649        if self.hazard_slot.is_some() {
650            return true;
651        }
652
653        if let Some(slot) = self.reclaimer.hazard.acquire_slot(self.thread_id) {
654            self.hazard_slot = Some(slot);
655            true
656        } else {
657            false
658        }
659    }
660
661    /// Protect a pointer with hazard pointer
662    pub fn protect(&self, ptr: *mut ()) {
663        if let Some(slot) = self.hazard_slot {
664            self.reclaimer.hazard.protect(slot, ptr);
665        }
666    }
667
668    /// Pin using epoch
669    pub fn pin_epoch(&self) {
670        self.reclaimer.epoch.pin(self.epoch_idx);
671    }
672
673    /// Unpin epoch
674    pub fn unpin_epoch(&self) {
675        self.reclaimer.epoch.unpin(self.epoch_idx);
676    }
677
678    /// Retire an object
679    pub fn retire(&self, ptr: *mut (), destructor: fn(*mut ()), size: usize) {
680        self.reclaimer.epoch.retire(ptr, destructor, size);
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687    use std::sync::atomic::AtomicBool;
688
689    #[test]
690    fn test_hazard_domain_basic() {
691        let domain = HazardDomain::new(4);
692
693        let thread_id = 12345u64;
694        let slot = domain.acquire_slot(thread_id).unwrap();
695
696        assert_eq!(domain.active_count(), 1);
697
698        // Protect a pointer
699        let data = Box::into_raw(Box::new(42u64));
700        domain.protect(slot, data as *mut ());
701
702        assert!(domain.is_protected(data as *mut ()));
703
704        domain.release_slot(thread_id, slot);
705        assert_eq!(domain.active_count(), 0);
706        assert!(!domain.is_protected(data as *mut ()));
707
708        // Cleanup
709        unsafe { drop(Box::from_raw(data)) };
710    }
711
712    #[test]
713    fn test_epoch_domain_basic() {
714        let domain = EpochDomain::new();
715
716        let idx = domain.register_thread();
717        assert_eq!(domain.current_epoch(), 0);
718
719        domain.pin(idx);
720        domain.advance_epoch();
721        assert_eq!(domain.current_epoch(), 1);
722
723        domain.unpin(idx);
724    }
725
726    #[test]
727    fn test_epoch_retirement() {
728        static DROPPED: AtomicBool = AtomicBool::new(false);
729        DROPPED.store(false, Ordering::SeqCst); // Reset for test isolation
730
731        fn drop_test(ptr: *mut ()) {
732            DROPPED.store(true, Ordering::SeqCst);
733            unsafe { drop(Box::from_raw(ptr as *mut u64)) };
734        }
735
736        let domain = EpochDomain::new();
737        let idx = domain.register_thread();
738
739        // Pin and retire
740        domain.pin(idx);
741        let data = Box::into_raw(Box::new(42u64));
742        domain.retire(data as *mut (), drop_test, 8);
743
744        // While pinned, try_reclaim should not reclaim (we're holding epoch 0)
745        // The safe_epoch will be at most epoch 0 - GRACE = underflow protection
746        let reclaimed_while_pinned = domain.try_reclaim();
747
748        // Unpin and advance epochs past grace period
749        domain.unpin(idx);
750        for _ in 0..EPOCH_GRACE_PERIODS + 2 {
751            domain.advance_epoch();
752        }
753
754        // Now should be reclaimed
755        let reclaimed = domain.try_reclaim();
756
757        // Either it was reclaimed now or during the retire threshold trigger
758        // The key test is that it eventually gets reclaimed
759        assert!(DROPPED.load(Ordering::SeqCst) || reclaimed > 0 || reclaimed_while_pinned > 0);
760    }
761
762    #[test]
763    fn test_unified_reclaimer_hazard() {
764        let reclaimer = UnifiedReclaimer::new();
765
766        let guard = reclaimer.pin_hazard();
767        assert!(guard.is_some());
768
769        let guard = guard.unwrap();
770        let data = Box::into_raw(Box::new(String::from("test")));
771        guard.protect_typed(data);
772
773        assert!(reclaimer.is_protected(data as *mut ()));
774
775        drop(guard);
776        assert!(!reclaimer.is_protected(data as *mut ()));
777
778        // Cleanup
779        unsafe { drop(Box::from_raw(data)) };
780    }
781
782    #[test]
783    fn test_unified_reclaimer_epoch() {
784        let reclaimer = UnifiedReclaimer::new();
785
786        {
787            let _guard = reclaimer.pin_epoch();
788            assert_eq!(reclaimer.current_epoch(), 0);
789        }
790
791        reclaimer.advance_epoch();
792        assert_eq!(reclaimer.current_epoch(), 1);
793    }
794
795    #[test]
796    fn test_stats_tracking() {
797        let reclaimer = UnifiedReclaimer::new();
798
799        {
800            let _guard = reclaimer.pin_epoch();
801        }
802
803        let _ = reclaimer.pin_hazard();
804
805        let stats = reclaimer.stats();
806        assert!(stats.epoch_pins >= 1);
807        assert!(stats.hazard_pins >= 1);
808    }
809
810    #[test]
811    fn test_thread_local_reclaimer() {
812        let reclaimer = Arc::new(UnifiedReclaimer::new());
813        let mut local = ThreadLocalReclaimer::new(Arc::clone(&reclaimer));
814
815        assert!(local.pin_hazard());
816
817        let data = Box::into_raw(Box::new(100u32));
818        local.protect(data as *mut ());
819
820        assert!(reclaimer.is_protected(data as *mut ()));
821
822        // Cleanup
823        unsafe { drop(Box::from_raw(data)) };
824    }
825
826    #[test]
827    fn test_multiple_hazard_slots() {
828        let domain = HazardDomain::new(2);
829
830        let slot1 = domain.acquire_slot(1).unwrap();
831        let slot2 = domain.acquire_slot(2).unwrap();
832
833        let data1 = Box::into_raw(Box::new(1u64));
834        let data2 = Box::into_raw(Box::new(2u64));
835
836        domain.protect(slot1, data1 as *mut ());
837        domain.protect(slot2, data2 as *mut ());
838
839        assert!(domain.is_protected(data1 as *mut ()));
840        assert!(domain.is_protected(data2 as *mut ()));
841
842        domain.release_slot(1, slot1);
843        assert!(!domain.is_protected(data1 as *mut ()));
844        assert!(domain.is_protected(data2 as *mut ()));
845
846        domain.release_slot(2, slot2);
847
848        // Cleanup
849        unsafe {
850            drop(Box::from_raw(data1));
851            drop(Box::from_raw(data2));
852        }
853    }
854
855    #[test]
856    fn test_reclaim_stats_snapshot() {
857        let stats = ReclaimStats::default();
858
859        stats.record_hazard_pin();
860        stats.record_hazard_pin();
861        stats.record_epoch_pin();
862        stats.record_retire();
863        stats.record_reclaim(5);
864
865        let snapshot = stats.snapshot();
866        assert_eq!(snapshot.hazard_pins, 2);
867        assert_eq!(snapshot.epoch_pins, 1);
868        assert_eq!(snapshot.objects_retired, 1);
869        assert_eq!(snapshot.objects_reclaimed, 5);
870        assert_eq!(snapshot.reclaim_cycles, 1);
871    }
872
873    #[test]
874    fn test_strategy_configuration() {
875        let reclaimer = UnifiedReclaimer::new().with_strategy(ReclaimStrategy::Epoch);
876
877        assert_eq!(reclaimer.default_strategy, ReclaimStrategy::Epoch);
878    }
879}