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}