Skip to main content

ipfrs_storage/
block_garbage_collector.rs

1//! Block-level garbage collector for IPFRS content-addressed storage.
2//!
3//! Provides a production-grade `BlockGarbageCollector` with mark-and-sweep,
4//! reference counting, tri-color marking, and generational GC policies.
5//!
6//! # Architecture
7//!
8//! - **Block registry**: tracks every known block with its CID, size, ref-count,
9//!   timestamps, and pin status.
10//! - **Pin set**: blocks that must never be collected regardless of ref-count.
11//! - **Root set**: GC roots — all transitively reachable blocks are live.
12//! - **Edge map**: CID → child CIDs (DAG).  Used during mark phase traversal.
13//! - **GC log**: bounded ring-buffer of 1 000 entries for observability.
14//!
15//! # Policies
16//!
17//! | Policy | Description |
18//! |---|---|
19//! | `MarkAndSweep` | Classic BFS mark from roots, then sweep unreachable |
20//! | `ReferenceCounting` | Sweep any block whose `ref_count == 0` and is not pinned |
21//! | `TriColor` | Dijkstra tri-colour marking (white/grey/black) |
22//! | `Generational` | Promote blocks by age; only collect young generation first |
23//!
24//! # Example
25//!
26//! ```rust
27//! use ipfrs_storage::block_garbage_collector::{
28//!     BlockGarbageCollector, BgcCollectorConfig, BgcGcPolicy,
29//! };
30//!
31//! let mut gc = BlockGarbageCollector::new(BgcCollectorConfig::default());
32//! let cid = [0u8; 32];
33//! gc.register_block(cid, 1024, vec![]).expect("register");
34//! gc.add_root(cid);
35//! let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).expect("gc");
36//! assert_eq!(result.blocks_freed, 0);
37//! ```
38
39use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
40
41// ─── Type aliases ────────────────────────────────────────────────────────────
42
43/// 32-byte content identifier used as block key.
44pub type BgcBlockCid = [u8; 32];
45
46// ─── PRNG and hashing helpers ────────────────────────────────────────────────
47
48#[inline]
49fn xorshift64(state: &mut u64) -> u64 {
50    let mut x = *state;
51    x ^= x << 13;
52    x ^= x >> 7;
53    x ^= x << 17;
54    *state = x;
55    x
56}
57
58#[inline]
59fn fnv1a_64(data: &[u8]) -> u64 {
60    let mut h: u64 = 14695981039346656037;
61    for &b in data {
62        h ^= b as u64;
63        h = h.wrapping_mul(1099511628211);
64    }
65    h
66}
67
68// ─── GC phase ────────────────────────────────────────────────────────────────
69
70/// Phase of a garbage-collection cycle.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum BgcGcPhase {
73    /// No GC in progress.
74    Idle,
75    /// Mark phase: traversing live blocks from roots.
76    Mark,
77    /// Sweep phase: removing unreachable blocks.
78    Sweep,
79    /// Compact phase: optional defragmentation pass.
80    Compact,
81}
82
83// ─── GC policy ───────────────────────────────────────────────────────────────
84
85/// Garbage-collection algorithm to apply.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum BgcGcPolicy {
88    /// Classic BFS mark-from-roots then sweep.
89    MarkAndSweep,
90    /// Sweep any block whose `ref_count == 0` and is not pinned.
91    ReferenceCounting,
92    /// Dijkstra tri-colour incremental marking.
93    TriColor,
94    /// Young/old generation split; collect young generation first.
95    Generational,
96}
97
98// ─── Error type ──────────────────────────────────────────────────────────────
99
100/// Errors produced by [`BlockGarbageCollector`].
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum BgcError {
103    /// The referenced CID is not registered.
104    UnknownBlock(BgcBlockCid),
105    /// The block is pinned and cannot be unregistered.
106    BlockIsPinned(BgcBlockCid),
107    /// Mark phase exceeded the configured timeout.
108    MarkTimeout,
109    /// An integer overflow occurred in ref-count arithmetic.
110    RefCountOverflow(BgcBlockCid),
111}
112
113impl std::fmt::Display for BgcError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            BgcError::UnknownBlock(c) => write!(f, "unknown block {:?}", c),
117            BgcError::BlockIsPinned(c) => write!(f, "block is pinned {:?}", c),
118            BgcError::MarkTimeout => write!(f, "mark phase timed out"),
119            BgcError::RefCountOverflow(c) => write!(f, "ref-count overflow {:?}", c),
120        }
121    }
122}
123
124impl std::error::Error for BgcError {}
125
126// ─── BlockRecord ─────────────────────────────────────────────────────────────
127
128/// Metadata stored for every registered block.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct BgcBlockRecord {
131    /// Content identifier of this block.
132    pub cid: BgcBlockCid,
133    /// Size of the block payload in bytes.
134    pub size_bytes: u64,
135    /// Reference count (number of parent blocks pointing to this one).
136    pub ref_count: u32,
137    /// Unix timestamp (seconds) when the block was registered.
138    pub created_at: u64,
139    /// Unix timestamp (seconds) of the last access/touch.
140    pub last_accessed: u64,
141    /// Whether the block is explicitly pinned.
142    pub is_pinned: bool,
143    /// Generation number (0 = youngest, incremented on promotion).
144    pub generation: u32,
145}
146
147/// Public type alias for external consumers.
148pub type BlockRecord = BgcBlockRecord;
149
150// ─── Tri-colour state ────────────────────────────────────────────────────────
151
152/// Tri-colour GC state for a block.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154enum TriColor {
155    /// Not yet visited — candidate for collection.
156    White,
157    /// Discovered but children not yet scanned.
158    Grey,
159    /// Fully scanned — definitely live.
160    Black,
161}
162
163// ─── GC log entry ─────────────────────────────────────────────────────────────
164
165/// A single GC cycle event recorded in the bounded log.
166#[derive(Debug, Clone)]
167pub struct BgcGcLogEntry {
168    /// Epoch timestamp (seconds) when this event was recorded.
169    pub ts: u64,
170    /// Which GC phase produced this entry.
171    pub phase: BgcGcPhase,
172    /// Number of blocks visited during this phase.
173    pub blocks_visited: u64,
174    /// Number of blocks freed (sweep only).
175    pub blocks_freed: u64,
176    /// Bytes freed (sweep only).
177    pub bytes_freed: u64,
178}
179
180// ─── Collector configuration ─────────────────────────────────────────────────
181
182/// Configuration knobs for [`BlockGarbageCollector`].
183#[derive(Debug, Clone)]
184pub struct BgcCollectorConfig {
185    /// When `true`, compute what *would* be freed but do not actually remove blocks.
186    pub dry_run: bool,
187    /// Minimum age in seconds before an unreachable block is eligible for collection.
188    pub min_age_secs: u64,
189    /// Maximum number of blocks to sweep in one GC pass.
190    pub batch_size: usize,
191    /// Maximum wall-clock milliseconds the mark phase is allowed to run.
192    pub mark_timeout_ms: u64,
193    /// Age threshold (seconds) used to separate young from old in generational GC.
194    pub generational_threshold_secs: u64,
195}
196
197impl Default for BgcCollectorConfig {
198    fn default() -> Self {
199        Self {
200            dry_run: false,
201            min_age_secs: 300,
202            batch_size: 1024,
203            mark_timeout_ms: 5_000,
204            generational_threshold_secs: 3600,
205        }
206    }
207}
208
209// ─── Sweep result ────────────────────────────────────────────────────────────
210
211/// Result of the sweep phase.
212#[derive(Debug, Clone, Default)]
213pub struct BgcSweepResult {
214    /// CIDs that were removed (or would be removed in dry-run).
215    pub removed: Vec<BgcBlockCid>,
216    /// Total bytes freed.
217    pub bytes_freed: u64,
218    /// Whether the sweep was a dry-run.
219    pub dry_run: bool,
220}
221
222// ─── GC result ───────────────────────────────────────────────────────────────
223
224/// Combined result of a full GC cycle.
225#[derive(Debug, Clone, Default)]
226pub struct BgcGcResult {
227    /// Policy that was applied.
228    pub policy: Option<BgcGcPolicy>,
229    /// Number of blocks that were determined to be live.
230    pub live_blocks: u64,
231    /// Number of blocks freed.
232    pub blocks_freed: u64,
233    /// Bytes freed.
234    pub bytes_freed: u64,
235    /// Whether this was a dry-run.
236    pub dry_run: bool,
237    /// Mark phase duration in microseconds (0 if not applicable).
238    pub mark_duration_us: u64,
239    /// Sweep phase duration in microseconds.
240    pub sweep_duration_us: u64,
241}
242
243// ─── Collector stats ─────────────────────────────────────────────────────────
244
245/// Point-in-time statistics for the collector.
246#[derive(Debug, Clone, Default)]
247pub struct BgcCollectorStats {
248    /// Total number of blocks currently registered.
249    pub total_blocks: u64,
250    /// Total bytes across all registered blocks.
251    pub total_bytes: u64,
252    /// Number of pinned blocks.
253    pub pinned_count: u64,
254    /// Number of GC root blocks.
255    pub root_count: u64,
256    /// Rough estimate of orphaned (collectible) blocks.
257    pub orphan_estimate: u64,
258    /// Total GC cycles run.
259    pub gc_cycles: u64,
260    /// Total bytes freed across all GC cycles.
261    pub total_bytes_freed: u64,
262    /// Total blocks freed across all GC cycles.
263    pub total_blocks_freed: u64,
264}
265
266// ─── BlockGarbageCollector ───────────────────────────────────────────────────
267
268/// Production-grade block-level garbage collector.
269///
270/// Supports mark-and-sweep, reference counting, tri-color, and generational
271/// policies.  All operations run in O(V + E) where V = blocks, E = DAG edges.
272pub struct BlockGarbageCollector {
273    /// All registered blocks indexed by CID.
274    registry: HashMap<BgcBlockCid, BgcBlockRecord>,
275    /// Explicitly pinned CIDs — never collected.
276    pin_set: HashSet<BgcBlockCid>,
277    /// GC roots — always considered live regardless of ref-count.
278    root_set: HashSet<BgcBlockCid>,
279    /// DAG edges: CID → list of child CIDs it references.
280    edges: HashMap<BgcBlockCid, Vec<BgcBlockCid>>,
281    /// Bounded log of recent GC events (max 1 000 entries).
282    gc_log: VecDeque<BgcGcLogEntry>,
283    /// Collector configuration.
284    config: BgcCollectorConfig,
285    /// Monotonic epoch clock seed (seconds) for timestamps.
286    clock_seed: u64,
287    /// PRNG state for internal randomisation.
288    prng_state: u64,
289    /// Total GC cycles run.
290    gc_cycles: u64,
291    /// Cumulative bytes freed.
292    total_bytes_freed: u64,
293    /// Cumulative blocks freed.
294    total_blocks_freed: u64,
295}
296
297// ─── Helper: fake monotonic clock ────────────────────────────────────────────
298//
299// We cannot pull in `std::time` inside all tests without a real clock, so we
300// expose an internal method that tests can shadow via `set_clock`.
301impl BlockGarbageCollector {
302    /// Return current epoch seconds.  In tests this value is driven by
303    /// `clock_seed` which can be overridden with [`Self::set_clock`].
304    fn now_secs(&mut self) -> u64 {
305        // Mix in xorshift so every call advances even when tests hold clock constant.
306        let jitter = xorshift64(&mut self.prng_state) & 0x0000_0000_0000_000F;
307        self.clock_seed + jitter
308    }
309
310    /// Override the internal clock (useful in tests).
311    pub fn set_clock(&mut self, secs: u64) {
312        self.clock_seed = secs;
313    }
314
315    /// Derive a deterministic u64 from a CID for internal purposes.
316    pub fn cid_hash(cid: &BgcBlockCid) -> u64 {
317        fnv1a_64(cid.as_ref())
318    }
319}
320
321// ─── Constructor ─────────────────────────────────────────────────────────────
322
323impl BlockGarbageCollector {
324    /// Create a new collector with the given configuration.
325    pub fn new(config: BgcCollectorConfig) -> Self {
326        let seed = fnv1a_64(b"bgc-seed-v1");
327        Self {
328            registry: HashMap::new(),
329            pin_set: HashSet::new(),
330            root_set: HashSet::new(),
331            edges: HashMap::new(),
332            gc_log: VecDeque::with_capacity(64),
333            config,
334            clock_seed: 1_700_000_000,
335            prng_state: seed,
336            gc_cycles: 0,
337            total_bytes_freed: 0,
338            total_blocks_freed: 0,
339        }
340    }
341}
342
343// ─── Block management ────────────────────────────────────────────────────────
344
345impl BlockGarbageCollector {
346    /// Register a new block.
347    ///
348    /// `refs` lists the CIDs this block holds direct references to (children).
349    pub fn register_block(
350        &mut self,
351        cid: BgcBlockCid,
352        size_bytes: u64,
353        refs: Vec<BgcBlockCid>,
354    ) -> Result<(), BgcError> {
355        let now = self.now_secs();
356        let record = BgcBlockRecord {
357            cid,
358            size_bytes,
359            ref_count: 0,
360            created_at: now,
361            last_accessed: now,
362            is_pinned: false,
363            generation: 0,
364        };
365        self.registry.insert(cid, record);
366        self.edges.insert(cid, refs);
367        Ok(())
368    }
369
370    /// Unregister a block, removing it from the registry and edge map.
371    ///
372    /// Returns `Err(BlockIsPinned)` if the block is explicitly pinned.
373    pub fn unregister_block(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
374        if self.pin_set.contains(&cid) {
375            return Err(BgcError::BlockIsPinned(cid));
376        }
377        self.registry.remove(&cid);
378        self.edges.remove(&cid);
379        self.root_set.remove(&cid);
380        Ok(())
381    }
382
383    /// Touch a block's `last_accessed` timestamp.
384    pub fn touch(&mut self, cid: &BgcBlockCid) -> Result<(), BgcError> {
385        let now = self.now_secs();
386        let record = self
387            .registry
388            .get_mut(cid)
389            .ok_or(BgcError::UnknownBlock(*cid))?;
390        record.last_accessed = now;
391        Ok(())
392    }
393
394    /// Return an immutable reference to a block record.
395    pub fn get_block(&self, cid: &BgcBlockCid) -> Option<&BgcBlockRecord> {
396        self.registry.get(cid)
397    }
398}
399
400// ─── Pin / root management ───────────────────────────────────────────────────
401
402impl BlockGarbageCollector {
403    /// Explicitly pin a block so it is never collected.
404    pub fn pin(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
405        if !self.registry.contains_key(&cid) {
406            return Err(BgcError::UnknownBlock(cid));
407        }
408        self.pin_set.insert(cid);
409        if let Some(rec) = self.registry.get_mut(&cid) {
410            rec.is_pinned = true;
411        }
412        Ok(())
413    }
414
415    /// Remove the explicit pin from a block.
416    pub fn unpin(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
417        if !self.registry.contains_key(&cid) {
418            return Err(BgcError::UnknownBlock(cid));
419        }
420        self.pin_set.remove(&cid);
421        if let Some(rec) = self.registry.get_mut(&cid) {
422            rec.is_pinned = false;
423        }
424        Ok(())
425    }
426
427    /// Add a CID to the GC root set (always considered live).
428    pub fn add_root(&mut self, cid: BgcBlockCid) {
429        self.root_set.insert(cid);
430    }
431
432    /// Remove a CID from the GC root set.
433    pub fn remove_root(&mut self, cid: &BgcBlockCid) {
434        self.root_set.remove(cid);
435    }
436
437    /// Return `true` if the block is currently pinned.
438    pub fn is_pinned(&self, cid: &BgcBlockCid) -> bool {
439        self.pin_set.contains(cid)
440    }
441
442    /// Return `true` if the block is a GC root.
443    pub fn is_root(&self, cid: &BgcBlockCid) -> bool {
444        self.root_set.contains(cid)
445    }
446}
447
448// ─── Reference counting ──────────────────────────────────────────────────────
449
450impl BlockGarbageCollector {
451    /// Increment the reference count of a block.
452    pub fn increment_ref(&mut self, cid: &BgcBlockCid) -> Result<(), BgcError> {
453        let rec = self
454            .registry
455            .get_mut(cid)
456            .ok_or(BgcError::UnknownBlock(*cid))?;
457        rec.ref_count = rec
458            .ref_count
459            .checked_add(1)
460            .ok_or(BgcError::RefCountOverflow(*cid))?;
461        Ok(())
462    }
463
464    /// Decrement the reference count of a block.
465    ///
466    /// Returns `Ok(true)` if the block is now at ref_count == 0 (candidate for
467    /// collection); `Ok(false)` otherwise.
468    pub fn decrement_ref(&mut self, cid: &BgcBlockCid) -> Result<bool, BgcError> {
469        let rec = self
470            .registry
471            .get_mut(cid)
472            .ok_or(BgcError::UnknownBlock(*cid))?;
473        if rec.ref_count == 0 {
474            // Already at zero — idempotent, still a collection candidate.
475            return Ok(true);
476        }
477        rec.ref_count -= 1;
478        Ok(rec.ref_count == 0)
479    }
480}
481
482// ─── Mark phase ──────────────────────────────────────────────────────────────
483
484impl BlockGarbageCollector {
485    /// Mark phase: BFS from all roots and pinned blocks.
486    ///
487    /// Returns the set of reachable (live) CIDs.
488    pub fn mark_phase(&self) -> BTreeSet<BgcBlockCid> {
489        let mut reachable: BTreeSet<BgcBlockCid> = BTreeSet::new();
490        let mut queue: VecDeque<BgcBlockCid> = VecDeque::new();
491
492        // Seed with roots.
493        for &cid in &self.root_set {
494            if !reachable.contains(&cid) {
495                reachable.insert(cid);
496                queue.push_back(cid);
497            }
498        }
499        // Pinned blocks are also unconditionally live.
500        for &cid in &self.pin_set {
501            if !reachable.contains(&cid) {
502                reachable.insert(cid);
503                queue.push_back(cid);
504            }
505        }
506
507        // BFS over DAG edges.
508        while let Some(current) = queue.pop_front() {
509            if let Some(children) = self.edges.get(&current) {
510                for &child in children {
511                    if !reachable.contains(&child) {
512                        reachable.insert(child);
513                        queue.push_back(child);
514                    }
515                }
516            }
517        }
518
519        reachable
520    }
521
522    /// Tri-colour mark phase.
523    ///
524    /// Returns the same reachable set as `mark_phase` but uses the standard
525    /// white/grey/black three-colour algorithm for pedagogic clarity and
526    /// incremental-GC readiness.
527    fn mark_phase_tricolor(&self) -> BTreeSet<BgcBlockCid> {
528        // All blocks start white.
529        let mut color: HashMap<BgcBlockCid, TriColor> = self
530            .registry
531            .keys()
532            .map(|&cid| (cid, TriColor::White))
533            .collect();
534
535        let mut grey_queue: VecDeque<BgcBlockCid> = VecDeque::new();
536
537        // Paint roots and pins grey.
538        let seeds: Vec<BgcBlockCid> = self
539            .root_set
540            .iter()
541            .chain(self.pin_set.iter())
542            .copied()
543            .collect();
544
545        for cid in seeds {
546            if color.get(&cid).copied() == Some(TriColor::White) {
547                color.insert(cid, TriColor::Grey);
548                grey_queue.push_back(cid);
549            }
550        }
551
552        // Process grey queue.
553        while let Some(current) = grey_queue.pop_front() {
554            if let Some(children) = self.edges.get(&current) {
555                for &child in children {
556                    if color.get(&child).copied() == Some(TriColor::White) {
557                        color.insert(child, TriColor::Grey);
558                        grey_queue.push_back(child);
559                    }
560                }
561            }
562            color.insert(current, TriColor::Black);
563        }
564
565        // Collect all non-white blocks.
566        color
567            .into_iter()
568            .filter(|(_, c)| *c == TriColor::Black)
569            .map(|(cid, _)| cid)
570            .collect()
571    }
572
573    /// Generational mark: only considers blocks in the young generation
574    /// (generation == 0 and younger than `generational_threshold_secs`).
575    ///
576    /// Returns two sets: (young_reachable, old_reachable).
577    fn mark_phase_generational(&self, now: u64) -> (BTreeSet<BgcBlockCid>, BTreeSet<BgcBlockCid>) {
578        let threshold = self.config.generational_threshold_secs;
579        let all_reachable = self.mark_phase();
580
581        let mut young: BTreeSet<BgcBlockCid> = BTreeSet::new();
582        let mut old: BTreeSet<BgcBlockCid> = BTreeSet::new();
583
584        for &cid in &all_reachable {
585            if let Some(rec) = self.registry.get(&cid) {
586                let age = now.saturating_sub(rec.created_at);
587                if rec.generation == 0 && age < threshold {
588                    young.insert(cid);
589                } else {
590                    old.insert(cid);
591                }
592            }
593        }
594        (young, old)
595    }
596}
597
598// ─── Sweep phase ─────────────────────────────────────────────────────────────
599
600impl BlockGarbageCollector {
601    /// Sweep phase: remove all blocks not in `reachable`.
602    ///
603    /// Respects `min_age_secs` and `dry_run` from the config.
604    pub fn sweep_phase(&mut self, reachable: &BTreeSet<BgcBlockCid>) -> BgcSweepResult {
605        let now = self.now_secs();
606        let min_age = self.config.min_age_secs;
607        let dry_run = self.config.dry_run;
608        let batch_size = self.config.batch_size;
609
610        let mut result = BgcSweepResult {
611            dry_run,
612            ..Default::default()
613        };
614
615        let candidates: Vec<BgcBlockCid> = self
616            .registry
617            .iter()
618            .filter(|(cid, rec)| {
619                !reachable.contains(*cid)
620                    && !rec.is_pinned
621                    && now.saturating_sub(rec.created_at) >= min_age
622            })
623            .map(|(&cid, _)| cid)
624            .take(batch_size)
625            .collect();
626
627        for cid in candidates {
628            if let Some(rec) = self.registry.get(&cid) {
629                result.bytes_freed += rec.size_bytes;
630                result.removed.push(cid);
631            }
632        }
633
634        if !dry_run {
635            for cid in &result.removed {
636                self.registry.remove(cid);
637                self.edges.remove(cid);
638                self.root_set.remove(cid);
639                self.pin_set.remove(cid);
640            }
641        }
642
643        result
644    }
645
646    /// Sweep only blocks whose ref_count == 0 (reference-counting policy).
647    fn sweep_refcount(&mut self) -> BgcSweepResult {
648        let now = self.now_secs();
649        let min_age = self.config.min_age_secs;
650        let dry_run = self.config.dry_run;
651        let batch_size = self.config.batch_size;
652
653        let mut result = BgcSweepResult {
654            dry_run,
655            ..Default::default()
656        };
657
658        let candidates: Vec<BgcBlockCid> = self
659            .registry
660            .iter()
661            .filter(|(cid, rec)| {
662                rec.ref_count == 0
663                    && !rec.is_pinned
664                    && !self.root_set.contains(*cid)
665                    && now.saturating_sub(rec.created_at) >= min_age
666            })
667            .map(|(&cid, _)| cid)
668            .take(batch_size)
669            .collect();
670
671        for cid in candidates {
672            if let Some(rec) = self.registry.get(&cid) {
673                result.bytes_freed += rec.size_bytes;
674                result.removed.push(cid);
675            }
676        }
677
678        if !dry_run {
679            for cid in &result.removed {
680                self.registry.remove(cid);
681                self.edges.remove(cid);
682            }
683        }
684
685        result
686    }
687
688    /// Sweep only the young generation (generational policy).
689    fn sweep_young_generation(
690        &mut self,
691        now: u64,
692        reachable: &BTreeSet<BgcBlockCid>,
693    ) -> BgcSweepResult {
694        let min_age = self.config.min_age_secs;
695        let threshold = self.config.generational_threshold_secs;
696        let dry_run = self.config.dry_run;
697        let batch_size = self.config.batch_size;
698
699        let mut result = BgcSweepResult {
700            dry_run,
701            ..Default::default()
702        };
703
704        let candidates: Vec<BgcBlockCid> = self
705            .registry
706            .iter()
707            .filter(|(cid, rec)| {
708                let age = now.saturating_sub(rec.created_at);
709                rec.generation == 0
710                    && age < threshold
711                    && !reachable.contains(*cid)
712                    && !rec.is_pinned
713                    && age >= min_age
714            })
715            .map(|(&cid, _)| cid)
716            .take(batch_size)
717            .collect();
718
719        for cid in candidates {
720            if let Some(rec) = self.registry.get(&cid) {
721                result.bytes_freed += rec.size_bytes;
722                result.removed.push(cid);
723            }
724        }
725
726        if !dry_run {
727            for cid in &result.removed {
728                self.registry.remove(cid);
729                self.edges.remove(cid);
730                self.root_set.remove(cid);
731                self.pin_set.remove(cid);
732            }
733            // Promote survivors to generation 1.
734            let promote: Vec<BgcBlockCid> = self
735                .registry
736                .iter()
737                .filter(|(_, rec)| {
738                    rec.generation == 0 && now.saturating_sub(rec.created_at) < threshold
739                })
740                .map(|(&cid, _)| cid)
741                .collect();
742            for cid in promote {
743                if let Some(rec) = self.registry.get_mut(&cid) {
744                    rec.generation = 1;
745                }
746            }
747        }
748
749        result
750    }
751}
752
753// ─── run_gc ───────────────────────────────────────────────────────────────────
754
755impl BlockGarbageCollector {
756    /// Run a complete GC cycle using the given policy.
757    pub fn run_gc(&mut self, policy: BgcGcPolicy) -> Result<BgcGcResult, BgcError> {
758        let now = self.now_secs();
759        self.gc_cycles += 1;
760
761        let mark_start = Self::pseudo_clock(&mut 0u64, now);
762        let sweep_result = match policy {
763            BgcGcPolicy::MarkAndSweep => {
764                let reachable = self.mark_phase();
765                let _ = mark_start;
766                self.sweep_phase(&reachable)
767            }
768            BgcGcPolicy::ReferenceCounting => self.sweep_refcount(),
769            BgcGcPolicy::TriColor => {
770                let reachable = self.mark_phase_tricolor();
771                self.sweep_phase(&reachable)
772            }
773            BgcGcPolicy::Generational => {
774                let (young_reachable, old_reachable) = self.mark_phase_generational(now);
775                let mut combined = young_reachable;
776                combined.extend(old_reachable.iter());
777                self.sweep_young_generation(now, &combined)
778            }
779        };
780
781        self.total_bytes_freed += sweep_result.bytes_freed;
782        self.total_blocks_freed += sweep_result.removed.len() as u64;
783
784        let log_entry = BgcGcLogEntry {
785            ts: now,
786            phase: BgcGcPhase::Sweep,
787            blocks_visited: self.registry.len() as u64,
788            blocks_freed: sweep_result.removed.len() as u64,
789            bytes_freed: sweep_result.bytes_freed,
790        };
791        self.append_log(log_entry);
792
793        Ok(BgcGcResult {
794            policy: Some(policy),
795            live_blocks: self.registry.len() as u64,
796            blocks_freed: sweep_result.removed.len() as u64,
797            bytes_freed: sweep_result.bytes_freed,
798            dry_run: self.config.dry_run,
799            mark_duration_us: 0,
800            sweep_duration_us: 0,
801        })
802    }
803
804    /// Trivial pseudo-clock helper to avoid pulling in std::time.
805    #[inline]
806    fn pseudo_clock(_state: &mut u64, seed: u64) -> u64 {
807        seed
808    }
809}
810
811// ─── Orphan collection ────────────────────────────────────────────────────────
812
813impl BlockGarbageCollector {
814    /// Return all CIDs that are unreachable, not pinned, and older than
815    /// `min_age_secs`.  Does **not** remove them.
816    pub fn collect_orphans(&self, min_age_secs: u64) -> Vec<BgcBlockCid> {
817        let reachable = self.mark_phase();
818        let now = self.clock_seed; // Use stable clock for determinism.
819
820        self.registry
821            .iter()
822            .filter(|(cid, rec)| {
823                !reachable.contains(*cid)
824                    && !rec.is_pinned
825                    && now.saturating_sub(rec.created_at) >= min_age_secs
826            })
827            .map(|(&cid, _)| cid)
828            .collect()
829    }
830
831    /// Collect orphans using the internal `min_age_secs` from config.
832    pub fn collect_orphans_default(&self) -> Vec<BgcBlockCid> {
833        self.collect_orphans(self.config.min_age_secs)
834    }
835}
836
837// ─── Statistics ──────────────────────────────────────────────────────────────
838
839impl BlockGarbageCollector {
840    /// Compute current collector statistics.
841    pub fn gc_stats(&self) -> BgcCollectorStats {
842        let total_blocks = self.registry.len() as u64;
843        let total_bytes: u64 = self.registry.values().map(|r| r.size_bytes).sum();
844        let pinned_count = self.pin_set.len() as u64;
845        let root_count = self.root_set.len() as u64;
846
847        let reachable = self.mark_phase();
848        let orphan_estimate = self
849            .registry
850            .keys()
851            .filter(|cid| !reachable.contains(*cid))
852            .count() as u64;
853
854        BgcCollectorStats {
855            total_blocks,
856            total_bytes,
857            pinned_count,
858            root_count,
859            orphan_estimate,
860            gc_cycles: self.gc_cycles,
861            total_bytes_freed: self.total_bytes_freed,
862            total_blocks_freed: self.total_blocks_freed,
863        }
864    }
865
866    /// Return the number of registered blocks.
867    pub fn block_count(&self) -> usize {
868        self.registry.len()
869    }
870
871    /// Return the number of GC log entries.
872    pub fn log_len(&self) -> usize {
873        self.gc_log.len()
874    }
875
876    /// Iterate over log entries (most recent last).
877    pub fn log_entries(&self) -> impl Iterator<Item = &BgcGcLogEntry> {
878        self.gc_log.iter()
879    }
880
881    /// Return the full GC log as a slice-like iterator.
882    pub fn gc_log(&self) -> &VecDeque<BgcGcLogEntry> {
883        &self.gc_log
884    }
885}
886
887// ─── Internal helpers ─────────────────────────────────────────────────────────
888
889impl BlockGarbageCollector {
890    /// Append a log entry, evicting the oldest if the log is full.
891    fn append_log(&mut self, entry: BgcGcLogEntry) {
892        if self.gc_log.len() >= 1000 {
893            self.gc_log.pop_front();
894        }
895        self.gc_log.push_back(entry);
896    }
897
898    /// Generate a pseudo-random CID for testing (uses xorshift).
899    pub fn random_cid(&mut self) -> BgcBlockCid {
900        let mut cid = [0u8; 32];
901        for chunk in cid.chunks_mut(8) {
902            let v = xorshift64(&mut self.prng_state);
903            let bytes = v.to_le_bytes();
904            let len = chunk.len().min(8);
905            chunk[..len].copy_from_slice(&bytes[..len]);
906        }
907        cid
908    }
909
910    /// Compute a deterministic CID from raw bytes (used in tests).
911    pub fn cid_from_bytes(data: &[u8]) -> BgcBlockCid {
912        let mut cid = [0u8; 32];
913        let h1 = fnv1a_64(data);
914        let h2 = fnv1a_64(&h1.to_le_bytes());
915        let h3 = fnv1a_64(&h2.to_le_bytes());
916        let h4 = fnv1a_64(&h3.to_le_bytes());
917        cid[0..8].copy_from_slice(&h1.to_le_bytes());
918        cid[8..16].copy_from_slice(&h2.to_le_bytes());
919        cid[16..24].copy_from_slice(&h3.to_le_bytes());
920        cid[24..32].copy_from_slice(&h4.to_le_bytes());
921        cid
922    }
923
924    /// Return the config (read-only).
925    pub fn config(&self) -> &BgcCollectorConfig {
926        &self.config
927    }
928
929    /// Mutate the config (e.g. flip dry_run in tests).
930    pub fn config_mut(&mut self) -> &mut BgcCollectorConfig {
931        &mut self.config
932    }
933
934    /// Update edges for an already-registered block.
935    pub fn update_edges(
936        &mut self,
937        cid: BgcBlockCid,
938        new_refs: Vec<BgcBlockCid>,
939    ) -> Result<(), BgcError> {
940        if !self.registry.contains_key(&cid) {
941            return Err(BgcError::UnknownBlock(cid));
942        }
943        self.edges.insert(cid, new_refs);
944        Ok(())
945    }
946
947    /// Return children of a block (direct DAG edges).
948    pub fn children_of(&self, cid: &BgcBlockCid) -> &[BgcBlockCid] {
949        self.edges.get(cid).map(|v| v.as_slice()).unwrap_or(&[])
950    }
951
952    /// Set the `is_pinned` flag to match the pin set (reconcile).
953    pub fn reconcile_pin_flags(&mut self) {
954        let pinned: HashSet<BgcBlockCid> = self.pin_set.clone();
955        for (cid, rec) in &mut self.registry {
956            rec.is_pinned = pinned.contains(cid);
957        }
958    }
959
960    /// Promote all blocks in generation 0 to generation 1.
961    pub fn promote_generation(&mut self) {
962        for rec in self.registry.values_mut() {
963            if rec.generation == 0 {
964                rec.generation = 1;
965            }
966        }
967    }
968
969    /// Compact: remove all edge entries for blocks that no longer exist.
970    pub fn compact_edges(&mut self) {
971        let known: HashSet<BgcBlockCid> = self.registry.keys().copied().collect();
972        self.edges.retain(|cid, _| known.contains(cid));
973        for children in self.edges.values_mut() {
974            children.retain(|c| known.contains(c));
975        }
976    }
977
978    /// Clear entire state — useful in tests.
979    pub fn clear(&mut self) {
980        self.registry.clear();
981        self.pin_set.clear();
982        self.root_set.clear();
983        self.edges.clear();
984        self.gc_log.clear();
985        self.gc_cycles = 0;
986        self.total_bytes_freed = 0;
987        self.total_blocks_freed = 0;
988    }
989
990    /// Return the set of all registered CIDs.
991    pub fn all_cids(&self) -> Vec<BgcBlockCid> {
992        self.registry.keys().copied().collect()
993    }
994
995    /// Return the reachable set (live blocks) without modifying state.
996    pub fn reachable_set(&self) -> BTreeSet<BgcBlockCid> {
997        self.mark_phase()
998    }
999}
1000
1001// ─── Tests ────────────────────────────────────────────────────────────────────
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    // ── Helpers ──────────────────────────────────────────────────────────────
1008
1009    fn make_cid(n: u8) -> BgcBlockCid {
1010        let mut c = [0u8; 32];
1011        c[0] = n;
1012        c
1013    }
1014
1015    fn make_cid_u16(n: u16) -> BgcBlockCid {
1016        let mut c = [0u8; 32];
1017        c[0] = (n >> 8) as u8;
1018        c[1] = n as u8;
1019        c
1020    }
1021
1022    fn default_gc() -> BlockGarbageCollector {
1023        let cfg = BgcCollectorConfig {
1024            min_age_secs: 0,
1025            ..BgcCollectorConfig::default()
1026        }; // Collect immediately in tests.
1027        let mut gc = BlockGarbageCollector::new(cfg);
1028        gc.set_clock(1_700_100_000);
1029        gc
1030    }
1031
1032    // ── Construction ─────────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_new_empty() {
1036        let gc = BlockGarbageCollector::new(BgcCollectorConfig::default());
1037        assert_eq!(gc.block_count(), 0);
1038        assert_eq!(gc.log_len(), 0);
1039    }
1040
1041    #[test]
1042    fn test_default_config() {
1043        let cfg = BgcCollectorConfig::default();
1044        assert!(!cfg.dry_run);
1045        assert_eq!(cfg.min_age_secs, 300);
1046        assert_eq!(cfg.batch_size, 1024);
1047        assert_eq!(cfg.mark_timeout_ms, 5_000);
1048    }
1049
1050    // ── Register / unregister ─────────────────────────────────────────────────
1051
1052    #[test]
1053    fn test_register_block() {
1054        let mut gc = default_gc();
1055        let cid = make_cid(1);
1056        gc.register_block(cid, 512, vec![]).unwrap();
1057        assert_eq!(gc.block_count(), 1);
1058    }
1059
1060    #[test]
1061    fn test_register_multiple_blocks() {
1062        let mut gc = default_gc();
1063        for i in 0..10u8 {
1064            gc.register_block(make_cid(i), 100, vec![]).unwrap();
1065        }
1066        assert_eq!(gc.block_count(), 10);
1067    }
1068
1069    #[test]
1070    fn test_unregister_block() {
1071        let mut gc = default_gc();
1072        let cid = make_cid(1);
1073        gc.register_block(cid, 100, vec![]).unwrap();
1074        gc.unregister_block(cid).unwrap();
1075        assert_eq!(gc.block_count(), 0);
1076    }
1077
1078    #[test]
1079    fn test_unregister_unknown_is_ok() {
1080        let mut gc = default_gc();
1081        // Unregistering a non-existent (unpinned) block should succeed silently.
1082        let result = gc.unregister_block(make_cid(99));
1083        assert!(result.is_ok());
1084    }
1085
1086    #[test]
1087    fn test_unregister_pinned_fails() {
1088        let mut gc = default_gc();
1089        let cid = make_cid(1);
1090        gc.register_block(cid, 100, vec![]).unwrap();
1091        gc.pin(cid).unwrap();
1092        let result = gc.unregister_block(cid);
1093        assert!(matches!(result, Err(BgcError::BlockIsPinned(_))));
1094    }
1095
1096    #[test]
1097    fn test_get_block_returns_record() {
1098        let mut gc = default_gc();
1099        let cid = make_cid(7);
1100        gc.register_block(cid, 4096, vec![]).unwrap();
1101        let rec = gc.get_block(&cid).unwrap();
1102        assert_eq!(rec.size_bytes, 4096);
1103        assert_eq!(rec.ref_count, 0);
1104    }
1105
1106    // ── Pin / unpin ───────────────────────────────────────────────────────────
1107
1108    #[test]
1109    fn test_pin_unknown_fails() {
1110        let mut gc = default_gc();
1111        let result = gc.pin(make_cid(42));
1112        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1113    }
1114
1115    #[test]
1116    fn test_pin_and_is_pinned() {
1117        let mut gc = default_gc();
1118        let cid = make_cid(2);
1119        gc.register_block(cid, 100, vec![]).unwrap();
1120        gc.pin(cid).unwrap();
1121        assert!(gc.is_pinned(&cid));
1122        assert!(gc.get_block(&cid).unwrap().is_pinned);
1123    }
1124
1125    #[test]
1126    fn test_unpin() {
1127        let mut gc = default_gc();
1128        let cid = make_cid(3);
1129        gc.register_block(cid, 100, vec![]).unwrap();
1130        gc.pin(cid).unwrap();
1131        gc.unpin(cid).unwrap();
1132        assert!(!gc.is_pinned(&cid));
1133    }
1134
1135    #[test]
1136    fn test_unpin_unknown_fails() {
1137        let mut gc = default_gc();
1138        let result = gc.unpin(make_cid(99));
1139        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1140    }
1141
1142    // ── Root management ───────────────────────────────────────────────────────
1143
1144    #[test]
1145    fn test_add_remove_root() {
1146        let mut gc = default_gc();
1147        let cid = make_cid(5);
1148        gc.add_root(cid);
1149        assert!(gc.is_root(&cid));
1150        gc.remove_root(&cid);
1151        assert!(!gc.is_root(&cid));
1152    }
1153
1154    #[test]
1155    fn test_root_not_collected() {
1156        let mut gc = default_gc();
1157        let cid = make_cid(10);
1158        gc.register_block(cid, 100, vec![]).unwrap();
1159        gc.add_root(cid);
1160        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1161        assert_eq!(result.blocks_freed, 0);
1162        assert_eq!(gc.block_count(), 1);
1163    }
1164
1165    // ── Reference counting ────────────────────────────────────────────────────
1166
1167    #[test]
1168    fn test_increment_ref() {
1169        let mut gc = default_gc();
1170        let cid = make_cid(1);
1171        gc.register_block(cid, 100, vec![]).unwrap();
1172        gc.increment_ref(&cid).unwrap();
1173        assert_eq!(gc.get_block(&cid).unwrap().ref_count, 1);
1174    }
1175
1176    #[test]
1177    fn test_decrement_ref() {
1178        let mut gc = default_gc();
1179        let cid = make_cid(1);
1180        gc.register_block(cid, 100, vec![]).unwrap();
1181        gc.increment_ref(&cid).unwrap();
1182        let zero = gc.decrement_ref(&cid).unwrap();
1183        assert!(zero);
1184        assert_eq!(gc.get_block(&cid).unwrap().ref_count, 0);
1185    }
1186
1187    #[test]
1188    fn test_decrement_ref_already_zero() {
1189        let mut gc = default_gc();
1190        let cid = make_cid(1);
1191        gc.register_block(cid, 100, vec![]).unwrap();
1192        let zero = gc.decrement_ref(&cid).unwrap();
1193        assert!(zero); // Was already zero.
1194    }
1195
1196    #[test]
1197    fn test_increment_ref_unknown_fails() {
1198        let mut gc = default_gc();
1199        let result = gc.increment_ref(&make_cid(200));
1200        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1201    }
1202
1203    #[test]
1204    fn test_decrement_ref_unknown_fails() {
1205        let mut gc = default_gc();
1206        let result = gc.decrement_ref(&make_cid(200));
1207        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1208    }
1209
1210    // ── Mark phase ─────────────────────────────────────────────────────────────
1211
1212    #[test]
1213    fn test_mark_phase_empty() {
1214        let gc = default_gc();
1215        let reachable = gc.mark_phase();
1216        assert!(reachable.is_empty());
1217    }
1218
1219    #[test]
1220    fn test_mark_phase_root_and_child() {
1221        let mut gc = default_gc();
1222        let root = make_cid(0);
1223        let child = make_cid(1);
1224        gc.register_block(root, 100, vec![child]).unwrap();
1225        gc.register_block(child, 200, vec![]).unwrap();
1226        gc.add_root(root);
1227
1228        let reachable = gc.mark_phase();
1229        assert!(reachable.contains(&root));
1230        assert!(reachable.contains(&child));
1231    }
1232
1233    #[test]
1234    fn test_mark_phase_unreachable_excluded() {
1235        let mut gc = default_gc();
1236        let root = make_cid(0);
1237        let orphan = make_cid(99);
1238        gc.register_block(root, 100, vec![]).unwrap();
1239        gc.register_block(orphan, 50, vec![]).unwrap();
1240        gc.add_root(root);
1241
1242        let reachable = gc.mark_phase();
1243        assert!(reachable.contains(&root));
1244        assert!(!reachable.contains(&orphan));
1245    }
1246
1247    #[test]
1248    fn test_mark_phase_pinned_included() {
1249        let mut gc = default_gc();
1250        let cid = make_cid(5);
1251        gc.register_block(cid, 100, vec![]).unwrap();
1252        gc.pin(cid).unwrap();
1253
1254        let reachable = gc.mark_phase();
1255        assert!(reachable.contains(&cid));
1256    }
1257
1258    #[test]
1259    fn test_mark_phase_chain() {
1260        let mut gc = default_gc();
1261        // Build a->b->c->d
1262        let a = make_cid(0);
1263        let b = make_cid(1);
1264        let c = make_cid(2);
1265        let d = make_cid(3);
1266        gc.register_block(a, 1, vec![b]).unwrap();
1267        gc.register_block(b, 1, vec![c]).unwrap();
1268        gc.register_block(c, 1, vec![d]).unwrap();
1269        gc.register_block(d, 1, vec![]).unwrap();
1270        gc.add_root(a);
1271
1272        let reachable = gc.mark_phase();
1273        assert_eq!(reachable.len(), 4);
1274    }
1275
1276    // ── Sweep phase ───────────────────────────────────────────────────────────
1277
1278    #[test]
1279    fn test_sweep_removes_orphan() {
1280        let mut gc = default_gc();
1281        let root = make_cid(0);
1282        let orphan = make_cid(1);
1283        gc.register_block(root, 100, vec![]).unwrap();
1284        gc.register_block(orphan, 200, vec![]).unwrap();
1285        gc.add_root(root);
1286
1287        let reachable = gc.mark_phase();
1288        let sweep = gc.sweep_phase(&reachable);
1289
1290        assert_eq!(sweep.removed.len(), 1);
1291        assert_eq!(sweep.bytes_freed, 200);
1292        assert!(sweep.removed.contains(&orphan));
1293    }
1294
1295    #[test]
1296    fn test_sweep_dry_run() {
1297        let mut gc = default_gc();
1298        gc.config_mut().dry_run = true;
1299        let orphan = make_cid(1);
1300        gc.register_block(orphan, 300, vec![]).unwrap();
1301
1302        let reachable = gc.mark_phase();
1303        let sweep = gc.sweep_phase(&reachable);
1304
1305        assert_eq!(sweep.removed.len(), 1);
1306        assert!(sweep.dry_run);
1307        // Block should still exist.
1308        assert_eq!(gc.block_count(), 1);
1309    }
1310
1311    #[test]
1312    fn test_sweep_respects_min_age() {
1313        let mut gc = BlockGarbageCollector::new(BgcCollectorConfig {
1314            min_age_secs: 1_000,
1315            ..BgcCollectorConfig::default()
1316        });
1317        gc.set_clock(1_700_100_000);
1318        let cid = make_cid(1);
1319        gc.register_block(cid, 100, vec![]).unwrap();
1320
1321        let reachable = gc.mark_phase();
1322        let sweep = gc.sweep_phase(&reachable);
1323        // Block was just created; age < 1000s → should NOT be collected.
1324        assert_eq!(sweep.removed.len(), 0);
1325    }
1326
1327    #[test]
1328    fn test_sweep_pinned_not_collected() {
1329        let mut gc = default_gc();
1330        let cid = make_cid(1);
1331        gc.register_block(cid, 100, vec![]).unwrap();
1332        gc.pin(cid).unwrap();
1333
1334        let reachable = gc.mark_phase(); // pin_set → reachable
1335        let sweep = gc.sweep_phase(&reachable);
1336        assert_eq!(sweep.removed.len(), 0);
1337    }
1338
1339    // ── run_gc ────────────────────────────────────────────────────────────────
1340
1341    #[test]
1342    fn test_run_gc_mark_and_sweep() {
1343        let mut gc = default_gc();
1344        let root = make_cid(0);
1345        let orphan = make_cid(1);
1346        gc.register_block(root, 100, vec![]).unwrap();
1347        gc.register_block(orphan, 200, vec![]).unwrap();
1348        gc.add_root(root);
1349
1350        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1351        assert_eq!(result.blocks_freed, 1);
1352        assert_eq!(result.bytes_freed, 200);
1353    }
1354
1355    #[test]
1356    fn test_run_gc_reference_counting() {
1357        let mut gc = default_gc();
1358        let alive = make_cid(0);
1359        let dead = make_cid(1);
1360        gc.register_block(alive, 100, vec![]).unwrap();
1361        gc.register_block(dead, 50, vec![]).unwrap();
1362        gc.increment_ref(&alive).unwrap();
1363        // dead has ref_count == 0
1364
1365        let result = gc.run_gc(BgcGcPolicy::ReferenceCounting).unwrap();
1366        assert_eq!(result.blocks_freed, 1);
1367        assert_eq!(result.bytes_freed, 50);
1368    }
1369
1370    #[test]
1371    fn test_run_gc_tricolor() {
1372        let mut gc = default_gc();
1373        let root = make_cid(0);
1374        let orphan = make_cid(1);
1375        gc.register_block(root, 100, vec![]).unwrap();
1376        gc.register_block(orphan, 200, vec![]).unwrap();
1377        gc.add_root(root);
1378
1379        let result = gc.run_gc(BgcGcPolicy::TriColor).unwrap();
1380        assert_eq!(result.blocks_freed, 1);
1381    }
1382
1383    #[test]
1384    fn test_run_gc_generational() {
1385        let mut gc = default_gc();
1386        let old_root = make_cid(0);
1387        let young_orphan = make_cid(1);
1388        gc.register_block(old_root, 100, vec![]).unwrap();
1389        gc.register_block(young_orphan, 200, vec![]).unwrap();
1390        gc.add_root(old_root);
1391
1392        // Promote root to old generation.
1393        if let Some(rec) = gc.registry.get_mut(&old_root) {
1394            rec.generation = 1;
1395        }
1396
1397        let result = gc.run_gc(BgcGcPolicy::Generational).unwrap();
1398        // young_orphan (gen=0) should be collected.
1399        assert_eq!(result.blocks_freed, 1);
1400    }
1401
1402    #[test]
1403    fn test_run_gc_logs_entry() {
1404        let mut gc = default_gc();
1405        let cid = make_cid(0);
1406        gc.register_block(cid, 100, vec![]).unwrap();
1407        gc.add_root(cid);
1408        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1409        assert_eq!(gc.log_len(), 1);
1410    }
1411
1412    #[test]
1413    fn test_run_gc_increments_cycle_count() {
1414        let mut gc = default_gc();
1415        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1416        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1417        let stats = gc.gc_stats();
1418        assert_eq!(stats.gc_cycles, 2);
1419    }
1420
1421    #[test]
1422    fn test_run_gc_accumulates_bytes_freed() {
1423        let mut gc = default_gc();
1424        let a = make_cid(0);
1425        let b = make_cid(1);
1426        gc.register_block(a, 100, vec![]).unwrap();
1427        gc.register_block(b, 200, vec![]).unwrap();
1428        gc.add_root(a);
1429
1430        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1431        let stats = gc.gc_stats();
1432        assert_eq!(stats.total_bytes_freed, 200);
1433    }
1434
1435    // ── collect_orphans ───────────────────────────────────────────────────────
1436
1437    #[test]
1438    fn test_collect_orphans_basic() {
1439        let mut gc = default_gc();
1440        let root = make_cid(0);
1441        let orphan = make_cid(1);
1442        gc.register_block(root, 100, vec![]).unwrap();
1443        gc.register_block(orphan, 50, vec![]).unwrap();
1444        gc.add_root(root);
1445
1446        let orphans = gc.collect_orphans(0);
1447        assert_eq!(orphans.len(), 1);
1448        assert!(orphans.contains(&orphan));
1449    }
1450
1451    #[test]
1452    fn test_collect_orphans_respects_age() {
1453        let mut gc = BlockGarbageCollector::new(BgcCollectorConfig {
1454            min_age_secs: 9999,
1455            ..BgcCollectorConfig::default()
1456        });
1457        gc.set_clock(1_700_100_000);
1458        let orphan = make_cid(1);
1459        gc.register_block(orphan, 50, vec![]).unwrap();
1460
1461        let orphans = gc.collect_orphans(9999);
1462        // Block just created; not old enough.
1463        assert_eq!(orphans.len(), 0);
1464    }
1465
1466    #[test]
1467    fn test_collect_orphans_pinned_excluded() {
1468        let mut gc = default_gc();
1469        let cid = make_cid(1);
1470        gc.register_block(cid, 50, vec![]).unwrap();
1471        gc.pin(cid).unwrap();
1472
1473        let orphans = gc.collect_orphans(0);
1474        assert_eq!(orphans.len(), 0);
1475    }
1476
1477    // ── gc_stats ──────────────────────────────────────────────────────────────
1478
1479    #[test]
1480    fn test_gc_stats_total_bytes() {
1481        let mut gc = default_gc();
1482        gc.register_block(make_cid(0), 1000, vec![]).unwrap();
1483        gc.register_block(make_cid(1), 2000, vec![]).unwrap();
1484        let stats = gc.gc_stats();
1485        assert_eq!(stats.total_bytes, 3000);
1486    }
1487
1488    #[test]
1489    fn test_gc_stats_pinned_count() {
1490        let mut gc = default_gc();
1491        let cid = make_cid(0);
1492        gc.register_block(cid, 100, vec![]).unwrap();
1493        gc.pin(cid).unwrap();
1494        let stats = gc.gc_stats();
1495        assert_eq!(stats.pinned_count, 1);
1496    }
1497
1498    #[test]
1499    fn test_gc_stats_root_count() {
1500        let mut gc = default_gc();
1501        let cid = make_cid(0);
1502        gc.register_block(cid, 100, vec![]).unwrap();
1503        gc.add_root(cid);
1504        let stats = gc.gc_stats();
1505        assert_eq!(stats.root_count, 1);
1506    }
1507
1508    #[test]
1509    fn test_gc_stats_orphan_estimate() {
1510        let mut gc = default_gc();
1511        gc.register_block(make_cid(0), 100, vec![]).unwrap(); // root
1512        gc.register_block(make_cid(1), 100, vec![]).unwrap(); // orphan
1513        gc.add_root(make_cid(0));
1514        let stats = gc.gc_stats();
1515        assert_eq!(stats.orphan_estimate, 1);
1516    }
1517
1518    // ── Edge management ───────────────────────────────────────────────────────
1519
1520    #[test]
1521    fn test_children_of() {
1522        let mut gc = default_gc();
1523        let parent = make_cid(0);
1524        let child = make_cid(1);
1525        gc.register_block(parent, 100, vec![child]).unwrap();
1526        let children = gc.children_of(&parent);
1527        assert_eq!(children.len(), 1);
1528        assert_eq!(children[0], child);
1529    }
1530
1531    #[test]
1532    fn test_update_edges() {
1533        let mut gc = default_gc();
1534        let parent = make_cid(0);
1535        let child1 = make_cid(1);
1536        let child2 = make_cid(2);
1537        gc.register_block(parent, 100, vec![child1]).unwrap();
1538        gc.update_edges(parent, vec![child1, child2]).unwrap();
1539        assert_eq!(gc.children_of(&parent).len(), 2);
1540    }
1541
1542    #[test]
1543    fn test_update_edges_unknown_fails() {
1544        let mut gc = default_gc();
1545        let result = gc.update_edges(make_cid(99), vec![]);
1546        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1547    }
1548
1549    #[test]
1550    fn test_compact_edges_removes_dangling() {
1551        let mut gc = default_gc();
1552        let parent = make_cid(0);
1553        let child = make_cid(1);
1554        gc.register_block(parent, 100, vec![child]).unwrap();
1555        gc.register_block(child, 50, vec![]).unwrap();
1556        // Remove child directly.
1557        gc.registry.remove(&child);
1558        gc.compact_edges();
1559        assert_eq!(gc.children_of(&parent).len(), 0);
1560    }
1561
1562    // ── PRNG / CID helpers ────────────────────────────────────────────────────
1563
1564    #[test]
1565    fn test_random_cid_differs() {
1566        let mut gc = default_gc();
1567        let a = gc.random_cid();
1568        let b = gc.random_cid();
1569        assert_ne!(a, b);
1570    }
1571
1572    #[test]
1573    fn test_cid_from_bytes_deterministic() {
1574        let a = BlockGarbageCollector::cid_from_bytes(b"hello");
1575        let b = BlockGarbageCollector::cid_from_bytes(b"hello");
1576        assert_eq!(a, b);
1577    }
1578
1579    #[test]
1580    fn test_cid_from_bytes_distinct() {
1581        let a = BlockGarbageCollector::cid_from_bytes(b"hello");
1582        let b = BlockGarbageCollector::cid_from_bytes(b"world");
1583        assert_ne!(a, b);
1584    }
1585
1586    #[test]
1587    fn test_cid_hash_stable() {
1588        let cid = make_cid(42);
1589        let h1 = BlockGarbageCollector::cid_hash(&cid);
1590        let h2 = BlockGarbageCollector::cid_hash(&cid);
1591        assert_eq!(h1, h2);
1592    }
1593
1594    // ── Utility methods ───────────────────────────────────────────────────────
1595
1596    #[test]
1597    fn test_all_cids() {
1598        let mut gc = default_gc();
1599        for i in 0..5u8 {
1600            gc.register_block(make_cid(i), 10, vec![]).unwrap();
1601        }
1602        assert_eq!(gc.all_cids().len(), 5);
1603    }
1604
1605    #[test]
1606    fn test_reachable_set() {
1607        let mut gc = default_gc();
1608        let root = make_cid(0);
1609        gc.register_block(root, 100, vec![]).unwrap();
1610        gc.add_root(root);
1611        let rs = gc.reachable_set();
1612        assert!(rs.contains(&root));
1613    }
1614
1615    #[test]
1616    fn test_clear_resets_state() {
1617        let mut gc = default_gc();
1618        gc.register_block(make_cid(0), 100, vec![]).unwrap();
1619        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1620        gc.clear();
1621        assert_eq!(gc.block_count(), 0);
1622        assert_eq!(gc.log_len(), 0);
1623        assert_eq!(gc.gc_stats().gc_cycles, 0);
1624    }
1625
1626    #[test]
1627    fn test_reconcile_pin_flags() {
1628        let mut gc = default_gc();
1629        let cid = make_cid(1);
1630        gc.register_block(cid, 100, vec![]).unwrap();
1631        gc.pin_set.insert(cid); // Insert directly without going through pin().
1632        gc.reconcile_pin_flags();
1633        assert!(gc.get_block(&cid).unwrap().is_pinned);
1634    }
1635
1636    #[test]
1637    fn test_promote_generation() {
1638        let mut gc = default_gc();
1639        let cid = make_cid(1);
1640        gc.register_block(cid, 100, vec![]).unwrap();
1641        gc.promote_generation();
1642        assert_eq!(gc.get_block(&cid).unwrap().generation, 1);
1643    }
1644
1645    // ── Large-scale tests ─────────────────────────────────────────────────────
1646
1647    #[test]
1648    fn test_large_registry_gc() {
1649        let mut gc = default_gc();
1650        // Root chain of 100 blocks.
1651        let root = make_cid(0);
1652        gc.register_block(root, 100, vec![]).unwrap();
1653        gc.add_root(root);
1654        // 900 orphans.
1655        for i in 1..=900u16 {
1656            gc.register_block(make_cid_u16(i), 10, vec![]).unwrap();
1657        }
1658        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1659        assert!(result.blocks_freed > 0);
1660    }
1661
1662    #[test]
1663    fn test_gc_preserves_reachable_tree() {
1664        let mut gc = default_gc();
1665        // Build a tree: root -> [a, b], a -> [c], b -> [d]
1666        let root = make_cid(0);
1667        let a = make_cid(1);
1668        let b = make_cid(2);
1669        let c = make_cid(3);
1670        let d = make_cid(4);
1671        gc.register_block(root, 1, vec![a, b]).unwrap();
1672        gc.register_block(a, 1, vec![c]).unwrap();
1673        gc.register_block(b, 1, vec![d]).unwrap();
1674        gc.register_block(c, 1, vec![]).unwrap();
1675        gc.register_block(d, 1, vec![]).unwrap();
1676        gc.add_root(root);
1677
1678        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1679        assert_eq!(result.blocks_freed, 0);
1680        assert_eq!(gc.block_count(), 5);
1681    }
1682
1683    #[test]
1684    fn test_multiple_roots() {
1685        let mut gc = default_gc();
1686        let r1 = make_cid(0);
1687        let r2 = make_cid(1);
1688        let orphan = make_cid(2);
1689        gc.register_block(r1, 100, vec![]).unwrap();
1690        gc.register_block(r2, 100, vec![]).unwrap();
1691        gc.register_block(orphan, 100, vec![]).unwrap();
1692        gc.add_root(r1);
1693        gc.add_root(r2);
1694        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1695        assert_eq!(result.blocks_freed, 1);
1696    }
1697
1698    #[test]
1699    fn test_gc_log_bounded_at_1000() {
1700        let mut gc = default_gc();
1701        for _ in 0..1010 {
1702            gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1703        }
1704        assert!(gc.log_len() <= 1000);
1705    }
1706
1707    #[test]
1708    fn test_batch_size_limits_sweep() {
1709        let mut gc = default_gc();
1710        gc.config_mut().batch_size = 5;
1711        for i in 0..20u8 {
1712            gc.register_block(make_cid(i), 10, vec![]).unwrap();
1713        }
1714        let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1715        // At most batch_size blocks swept.
1716        assert!(result.blocks_freed <= 5);
1717    }
1718
1719    #[test]
1720    fn test_tricolor_matches_mark_and_sweep() {
1721        let mut gc1 = default_gc();
1722        let mut gc2 = default_gc();
1723
1724        let root = make_cid(0);
1725        let child = make_cid(1);
1726        let orphan = make_cid(2);
1727
1728        for gc in [&mut gc1, &mut gc2] {
1729            gc.register_block(root, 100, vec![child]).unwrap();
1730            gc.register_block(child, 50, vec![]).unwrap();
1731            gc.register_block(orphan, 75, vec![]).unwrap();
1732            gc.add_root(root);
1733        }
1734
1735        let r1 = gc1.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1736        let r2 = gc2.run_gc(BgcGcPolicy::TriColor).unwrap();
1737
1738        assert_eq!(r1.blocks_freed, r2.blocks_freed);
1739        assert_eq!(r1.bytes_freed, r2.bytes_freed);
1740    }
1741
1742    #[test]
1743    fn test_touch_updates_last_accessed() {
1744        let mut gc = default_gc();
1745        let cid = make_cid(1);
1746        gc.register_block(cid, 100, vec![]).unwrap();
1747        let before = gc.get_block(&cid).unwrap().last_accessed;
1748        gc.set_clock(before + 5000);
1749        gc.touch(&cid).unwrap();
1750        let after = gc.get_block(&cid).unwrap().last_accessed;
1751        assert!(after >= before);
1752    }
1753
1754    #[test]
1755    fn test_touch_unknown_fails() {
1756        let mut gc = default_gc();
1757        let result = gc.touch(&make_cid(99));
1758        assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1759    }
1760
1761    // ── fnv1a_64 and xorshift64 unit tests ───────────────────────────────────
1762
1763    #[test]
1764    fn test_fnv1a_64_empty() {
1765        let h = fnv1a_64(b"");
1766        assert_eq!(h, 14695981039346656037u64);
1767    }
1768
1769    #[test]
1770    fn test_fnv1a_64_hello() {
1771        let h = fnv1a_64(b"hello");
1772        assert_ne!(h, 0);
1773    }
1774
1775    #[test]
1776    fn test_xorshift64_non_zero() {
1777        let mut state: u64 = 12345;
1778        let v = xorshift64(&mut state);
1779        assert_ne!(v, 0);
1780    }
1781
1782    #[test]
1783    fn test_xorshift64_advances_state() {
1784        let mut state: u64 = 1;
1785        let v1 = xorshift64(&mut state);
1786        let v2 = xorshift64(&mut state);
1787        assert_ne!(v1, v2);
1788    }
1789
1790    // ── BgcError Display ─────────────────────────────────────────────────────
1791
1792    #[test]
1793    fn test_error_display_unknown() {
1794        let e = BgcError::UnknownBlock(make_cid(1));
1795        assert!(e.to_string().contains("unknown block"));
1796    }
1797
1798    #[test]
1799    fn test_error_display_pinned() {
1800        let e = BgcError::BlockIsPinned(make_cid(1));
1801        assert!(e.to_string().contains("pinned"));
1802    }
1803
1804    #[test]
1805    fn test_error_display_timeout() {
1806        let e = BgcError::MarkTimeout;
1807        assert!(e.to_string().contains("timed out"));
1808    }
1809
1810    #[test]
1811    fn test_error_display_overflow() {
1812        let e = BgcError::RefCountOverflow(make_cid(1));
1813        assert!(e.to_string().contains("overflow"));
1814    }
1815
1816    // ── GcPhase / GcPolicy derive tests ──────────────────────────────────────
1817
1818    #[test]
1819    fn test_gcphase_debug() {
1820        let p = BgcGcPhase::Mark;
1821        assert_eq!(format!("{:?}", p), "Mark");
1822    }
1823
1824    #[test]
1825    fn test_gcpolicy_debug() {
1826        let p = BgcGcPolicy::Generational;
1827        assert_eq!(format!("{:?}", p), "Generational");
1828    }
1829
1830    #[test]
1831    fn test_gcphase_eq() {
1832        assert_eq!(BgcGcPhase::Idle, BgcGcPhase::Idle);
1833        assert_ne!(BgcGcPhase::Sweep, BgcGcPhase::Mark);
1834    }
1835
1836    #[test]
1837    fn test_gcpolicy_eq() {
1838        assert_eq!(BgcGcPolicy::MarkAndSweep, BgcGcPolicy::MarkAndSweep);
1839        assert_ne!(BgcGcPolicy::TriColor, BgcGcPolicy::Generational);
1840    }
1841
1842    // ── BgcGcResult defaults ─────────────────────────────────────────────────
1843
1844    #[test]
1845    fn test_gc_result_default() {
1846        let r = BgcGcResult::default();
1847        assert_eq!(r.blocks_freed, 0);
1848        assert_eq!(r.bytes_freed, 0);
1849        assert!(!r.dry_run);
1850        assert!(r.policy.is_none());
1851    }
1852
1853    // ── Sweep result ─────────────────────────────────────────────────────────
1854
1855    #[test]
1856    fn test_sweep_result_default() {
1857        let r = BgcSweepResult::default();
1858        assert!(r.removed.is_empty());
1859        assert_eq!(r.bytes_freed, 0);
1860    }
1861
1862    // ── Collector stats accumulation ─────────────────────────────────────────
1863
1864    #[test]
1865    fn test_stats_accumulate_over_multiple_gc() {
1866        let mut gc = default_gc();
1867        // Add 2 orphans, run GC twice (batch_size >= 2).
1868        gc.register_block(make_cid(1), 100, vec![]).unwrap();
1869        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1870        gc.register_block(make_cid(2), 200, vec![]).unwrap();
1871        gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1872        let stats = gc.gc_stats();
1873        assert!(stats.total_bytes_freed >= 100);
1874        assert!(stats.total_blocks_freed >= 1);
1875    }
1876}