Skip to main content

loonfs_core/gc/
budget.rs

1//! What `max_objects` meters, and what a pass does when it runs out.
2
3use super::config::GcConfig;
4
5/// The work one garbage-collection pass is allowed to do.
6///
7/// One unit is one object the pass reads or decides on a candidate's
8/// behalf:
9///
10/// * one enumerated candidate key, decided — the original meaning of
11///   `max_objects`;
12/// * one manifest opened by the content reference scan;
13/// * one page of revision rows read out of an opened manifest;
14/// * one retained WAL segment fetched by the scan.
15///
16/// Prefix listing is the one thing not metered: it is how a family finds
17/// candidates at all, and the cursor is what resumes it. Everything else a
18/// pass touches is charged, so no pass can do work proportional to the
19/// namespace while asking for a small budget.
20///
21/// The rule is deliberately coarse — it is a bound, not a cost model. A
22/// page of rows costs more than a manifest header and both count as one.
23/// What matters is that every store round trip inside the pass has a
24/// charge attached to it, and that running out stops the pass instead of
25/// letting it finish "just this one thing" unboundedly.
26#[derive(Debug)]
27pub struct PassBudget {
28    max_objects: Option<u64>,
29    spent: u64,
30}
31
32impl PassBudget {
33    /// Meters a pass at `max_objects` units, or at nothing when absent.
34    pub fn new(max_objects: Option<u64>) -> Self {
35        Self {
36            max_objects,
37            spent: 0,
38        }
39    }
40
41    pub(super) fn of(config: &GcConfig) -> Self {
42        Self::new(config.max_objects)
43    }
44
45    /// True once nothing further may be charged: the caller stops where it
46    /// stands. The sweep returns its cursor; the content reference scan
47    /// gives up on collecting and the pass defers that reclamation.
48    pub fn exhausted(&self) -> bool {
49        self.max_objects
50            .is_some_and(|max_objects| self.spent >= max_objects)
51    }
52
53    /// Charges one unit for work already done.
54    pub fn charge(&mut self) {
55        self.spent = self.spent.saturating_add(1);
56    }
57
58    /// Charges one unit for work about to be done. `false` means the
59    /// budget is spent and the caller must not do it.
60    pub(super) fn try_charge(&mut self) -> bool {
61        if self.exhausted() {
62            return false;
63        }
64        self.charge();
65        true
66    }
67}