Skip to main content

heddle_pack/store/pack/repack/
policy.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Cheap storage facts used to decide whether a repack is worthwhile.
4#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5pub struct RepackInventory {
6    /// Objects currently stored loose.
7    pub loose_objects: u64,
8    /// On-disk bytes occupied by loose objects.
9    pub loose_bytes: u64,
10    /// Active pack count.
11    pub pack_count: u64,
12    /// Bytes occupied by active pack and index files.
13    pub pack_bytes: u64,
14    /// Pack entries whose identity also occurs in another active pack.
15    pub duplicate_objects: u64,
16    /// Total entries across active packs, including duplicates.
17    pub packed_objects: u64,
18}
19
20impl RepackInventory {
21    /// Duplicate-entry fragmentation in basis points (`10_000 == 100%`).
22    pub fn fragmentation_bps(self) -> u16 {
23        if self.packed_objects == 0 {
24            return 0;
25        }
26        let bps = self
27            .duplicate_objects
28            .saturating_mul(10_000)
29            .checked_div(self.packed_objects)
30            .unwrap_or(0);
31        bps.min(10_000) as u16
32    }
33}
34
35/// The policy signal that caused a background repack.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum RepackReason {
38    /// An operator explicitly requested a repack.
39    Manual,
40    /// The loose-object threshold was crossed.
41    LooseObjects { count: u64 },
42    /// The active pack-count threshold was crossed.
43    PackCount { count: u64 },
44    /// Multiple packs crossed the combined-size threshold.
45    PackBytes { bytes: u64 },
46    /// Duplicate entries crossed the fragmentation threshold.
47    Fragmentation { basis_points: u16 },
48}
49
50/// Configurable automatic repack thresholds.
51///
52/// `None` disables a signal. Size only triggers when at least two packs exist:
53/// rewriting one already-consolidated large pack would otherwise thrash forever.
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub struct RepackPolicy {
56    /// Trigger after this many loose objects.
57    pub loose_object_threshold: Option<u64>,
58    /// Trigger after this many active packs.
59    pub pack_count_threshold: Option<u64>,
60    /// Trigger when two or more packs occupy at least this many bytes.
61    pub pack_bytes_threshold: Option<u64>,
62    /// Trigger at this duplicate-entry ratio in basis points.
63    pub fragmentation_threshold_bps: Option<u16>,
64}
65
66impl Default for RepackPolicy {
67    fn default() -> Self {
68        Self {
69            loose_object_threshold: Some(10_000),
70            pack_count_threshold: Some(8),
71            pack_bytes_threshold: Some(1024 * 1024 * 1024),
72            fragmentation_threshold_bps: Some(1_500),
73        }
74    }
75}
76
77impl RepackPolicy {
78    /// Return the first policy signal crossed by `inventory`.
79    pub fn evaluate(self, inventory: RepackInventory) -> Option<RepackReason> {
80        if self
81            .loose_object_threshold
82            .is_some_and(|threshold| inventory.loose_objects >= threshold)
83        {
84            return Some(RepackReason::LooseObjects {
85                count: inventory.loose_objects,
86            });
87        }
88        if self
89            .pack_count_threshold
90            .is_some_and(|threshold| inventory.pack_count >= threshold)
91        {
92            return Some(RepackReason::PackCount {
93                count: inventory.pack_count,
94            });
95        }
96        if inventory.pack_count > 1
97            && self
98                .pack_bytes_threshold
99                .is_some_and(|threshold| inventory.pack_bytes >= threshold)
100        {
101            return Some(RepackReason::PackBytes {
102                bytes: inventory.pack_bytes,
103            });
104        }
105        let fragmentation = inventory.fragmentation_bps();
106        if self
107            .fragmentation_threshold_bps
108            .is_some_and(|threshold| fragmentation >= threshold)
109        {
110            return Some(RepackReason::Fragmentation {
111                basis_points: fragmentation,
112            });
113        }
114        None
115    }
116}