tor_memquota/
config.rs

1//! Configuration (private module)
2
3use crate::internal_prelude::*;
4
5/// We want to support at least this many participants with a cache each
6///
7/// This is not a recommended value; it's probably too lax
8const MIN_MAX_PARTICIPANTS: usize = 10;
9
10/// Minimum hysteresis
11///
12/// This is not a recommended value; it's probably far too lax for sensible performance!
13const MAX_LOW_WATER_RATIO: f32 = 0.98;
14
15define_derive_deftly! {
16    /// Define setters on the builder for every field of type `Qty`
17    ///
18    /// The field type must be spelled precisely that way:
19    /// we use `approx_equal(...)`.
20    QtySetters:
21
22    impl ConfigBuilder {
23      $(
24        ${when approx_equal($ftype, { Option::<Qty> })}
25
26        ${fattrs doc}
27        ///
28        /// (Setter method.)
29        pub fn $fname(&mut self, value: usize) -> &mut Self {
30            self.$fname = Some(Qty(value));
31            self
32        }
33      )
34    }
35}
36
37/// Configuration for a memory data tracker
38///
39/// This is where the quota is specified.
40///
41/// This type can also represent
42/// "memory quota tracking is not supposed to be enabled".
43#[derive(Debug, Clone, Eq, PartialEq)]
44pub struct Config(pub(crate) IfEnabled<ConfigInner>);
45
46/// Configuration for a memory data tracker (builder)
47//
48// We could perhaps generate this with `#[derive(Builder)]` on `ConfigInner`,
49// but derive-builder would need a *lot* of overriding attributes;
50// and, doing it this way lets us write separate docs about
51// the invariants on our fields, which are not the same as those in the builder.
52#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default, Deftly)]
53#[derive_deftly(tor_config::Flattenable, QtySetters)]
54pub struct ConfigBuilder {
55    /// Maximum memory usage tolerated before reclamation starts
56    ///
57    /// Setting this to `usize::MAX` disables the memory quota
58    /// (and that's the default).
59    ///
60    /// Note that this is not a hard limit.
61    /// See Approximate in [the overview](crate).
62    max: Option<Qty>,
63
64    /// Reclamation will stop when memory use is reduced to below this value
65    ///
66    /// Default is 75% of the maximum.
67    low_water: Option<Qty>,
68}
69
70/// Configuration, if enabled
71#[derive(Debug, Clone, Eq, PartialEq, Deftly)]
72#[cfg_attr(
73    feature = "testing",
74    visibility::make(pub),
75    allow(clippy::exhaustive_structs)
76)]
77pub(crate) struct ConfigInner {
78    /// Maximum memory usage
79    ///
80    /// Guaranteed not to be `MAX`, since we're anbled
81    pub max: Qty,
82
83    /// Low water
84    ///
85    /// Guaranteed to be enough lower than `max`
86    pub low_water: Qty,
87}
88
89impl Config {
90    /// Start building a [`Config`]
91    ///
92    /// Returns a fresh default [`ConfigBuilder`].
93    pub fn builder() -> ConfigBuilder {
94        ConfigBuilder::default()
95    }
96
97    /// Obtain the actual configuration, if we're enabled, or `None` if not
98    ///
99    /// Ad-hoc accessor for testing purposes.
100    /// (ideally we'd use `visibility` to make fields `pub`, but that doesn't work.)
101    #[cfg(feature = "testing")]
102    pub fn inner(&self) -> Option<&ConfigInner> {
103        self.0.as_ref().into_enabled()
104    }
105}
106
107impl ConfigBuilder {
108    /// Builds a new `Config` from a builder
109    ///
110    /// Returns an error unless at least `max` has been specified,
111    /// or if the fields values are invalid or inconsistent.
112    pub fn build(&self) -> Result<Config, ConfigBuildError> {
113        let max = self.max.unwrap_or(Qty::MAX);
114
115        if max == Qty::MAX {
116            if self.low_water.is_some() {
117                return Err(ConfigBuildError::Inconsistent {
118                    fields: vec!["max".into(), "low_water".into()],
119                    problem: "low_water supplied, but max omitted".into(),
120                });
121            };
122            return Ok(Config(IfEnabled::Noop));
123        }
124
125        let enabled = EnabledToken::new_if_compiled_in()
126            //
127            .ok_or_else(|| ConfigBuildError::NoCompileTimeSupport {
128                field: "max".into(),
129                problem: "cargo feature `memquota` disabled (in tor-memquota crate)".into(),
130            })?;
131
132        let low_water = self.low_water.unwrap_or_else(
133            //
134            || Qty((*max as f32 * 0.75) as _),
135        );
136
137        let config = ConfigInner { max, low_water };
138
139        /// Minimum low water.  `const` so that overflows are compile-time.
140        const MIN_LOW_WATER: usize = crate::mtracker::MAX_CACHE.as_usize() * MIN_MAX_PARTICIPANTS;
141        let min_low_water = MIN_LOW_WATER;
142        if *config.low_water < min_low_water {
143            return Err(ConfigBuildError::Invalid {
144                field: "low_water".into(),
145                problem: format!("must be at least {min_low_water}"),
146            });
147        }
148
149        let ratio: f32 = *config.low_water as f32 / *config.max as f32;
150        if ratio > MAX_LOW_WATER_RATIO {
151            return Err(ConfigBuildError::Inconsistent {
152                fields: vec!["low_water".into(), "max".into()],
153                problem: format!(
154 "low_water / max = {ratio}; must be <= {MAX_LOW_WATER_RATIO}, ideally considerably lower"
155                ),
156            });
157        }
158
159        Ok(Config(IfEnabled::Enabled(config, enabled)))
160    }
161}
162
163#[cfg(test)]
164mod test {
165    // @@ begin test lint list maintained by maint/add_warning @@
166    #![allow(clippy::bool_assert_comparison)]
167    #![allow(clippy::clone_on_copy)]
168    #![allow(clippy::dbg_macro)]
169    #![allow(clippy::mixed_attributes_style)]
170    #![allow(clippy::print_stderr)]
171    #![allow(clippy::print_stdout)]
172    #![allow(clippy::single_char_pattern)]
173    #![allow(clippy::unwrap_used)]
174    #![allow(clippy::unchecked_duration_subtraction)]
175    #![allow(clippy::useless_vec)]
176    #![allow(clippy::needless_pass_by_value)]
177    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
178
179    use super::*;
180    use serde_json::json;
181
182    #[test]
183    fn configs() {
184        let chk_ok_raw = |j, c| {
185            let b: ConfigBuilder = serde_json::from_value(j).unwrap();
186            assert_eq!(b.build().unwrap(), c);
187        };
188        #[cfg(feature = "memquota")]
189        let chk_ok = |j, max, low_water| {
190            const M: usize = 1024 * 1024;
191
192            let exp = IfEnabled::Enabled(
193                ConfigInner {
194                    max: Qty(max * M),
195                    low_water: Qty(low_water * M),
196                },
197                EnabledToken::new(),
198            );
199
200            chk_ok_raw(j, Config(exp));
201        };
202        let chk_err = |j, exp| {
203            let b: ConfigBuilder = serde_json::from_value(j).unwrap();
204            let got = b.build().unwrap_err().to_string();
205
206            #[cfg(not(feature = "memquota"))]
207            if got.contains("cargo feature `memquota` disabled") {
208                return;
209            }
210
211            assert!(got.contains(exp), "in {exp:?} in {got:?}");
212        };
213        #[cfg(not(feature = "memquota"))]
214        let chk_ok = |j, max, low_water| {
215            chk_err(j, "UNSUPPORTED");
216        };
217
218        chk_ok(json! {{ "max": "8 MiB" }}, 8, 6);
219        chk_ok(json! {{ "max": "8 MiB", "low_water": "4 MiB" }}, 8, 4);
220        chk_ok_raw(json! {{ }}, Config(IfEnabled::Noop));
221
222        chk_err(
223            json! {{ "low_water": "4 MiB" }},
224            "low_water supplied, but max omitted",
225        );
226        chk_err(
227            json! {{ "max": "8 MiB", "low_water": "8 MiB" }},
228            "inconsistent: low_water / max",
229        );
230    }
231}