summaryrefslogtreecommitdiff
path: root/crates/tor-memquota/src/config.rs
blob: 763316b64bc0956c6627eeccbb36bfbdcbc5d6fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Configuration (private module)

use crate::internal_prelude::*;

/// We want to support at least this many participants with a cache each
///
/// This is not a recommended value; it's probably too lax
const MIN_MAX_PARTICIPANTS: usize = 10;

/// Minimum hysteresis
///
/// This is not a recommended value; it's probably far too lax for sensible performance!
const MAX_LOW_WATER_RATIO: f32 = 0.98;

define_derive_deftly! {
    /// Define setters on the builder for every field of type `Qty`
    ///
    /// The field type must be spelled precisely that way:
    /// we use `approx_equal(...)`.
    QtySetters:

    impl ConfigBuilder {
      $(
        ${when approx_equal($ftype, { Option::<Qty> })}

        ${fattrs doc}
        ///
        /// (Setter method.)
        pub fn $fname(&mut self, value: usize) -> &mut Self {
            self.$fname = Some(Qty(value));
            self
        }
      )
    }
}

/// Configuration for a memory data tracker
///
/// This is where the quota is specified.
///
/// This type can also represent
/// "memory quota tracking is not supposed to be enabled".
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Config(pub(crate) IfEnabled<ConfigInner>);

/// Configuration for a memory data tracker (builder)
//
// We could perhaps generate this with `#[derive(Builder)]` on `ConfigInner`,
// but derive-builder would need a *lot* of overriding attributes;
// and, doing it this way lets us write separate docs about
// the invariants on our fields, which are not the same as those in the builder.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default, Deftly)]
#[derive_deftly(tor_config::Flattenable, QtySetters)]
pub struct ConfigBuilder {
    /// Maximum memory usage tolerated before reclamation starts
    ///
    /// Setting this to `usize::MAX` disables the memory quota
    /// (and that's the default).
    ///
    /// Note that this is not a hard limit.
    /// See Approximate in [the overview](crate).
    max: Option<Qty>,

    /// Reclamation will stop when memory use is reduced to below this value
    ///
    /// Default is 75% of the maximum.
    low_water: Option<Qty>,
}

/// Configuration, if enabled
#[derive(Debug, Clone, Eq, PartialEq, Deftly)]
#[cfg_attr(
    feature = "testing",
    visibility::make(pub),
    allow(clippy::exhaustive_structs)
)]
pub(crate) struct ConfigInner {
    /// Maximum memory usage
    ///
    /// Guaranteed not to be `MAX`, since we're anbled
    pub max: Qty,

    /// Low water
    ///
    /// Guaranteed to be enough lower than `max`
    pub low_water: Qty,
}

impl Config {
    /// Start building a [`Config`]
    ///
    /// Returns a fresh default [`ConfigBuilder`].
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }

    /// Obtain the actual configuration, if we're enabled, or `None` if not
    ///
    /// Ad-hoc accessor for testing purposes.
    /// (ideally we'd use `visibility` to make fields `pub`, but that doesn't work.)
    #[cfg(feature = "testing")]
    pub fn inner(&self) -> Option<&ConfigInner> {
        self.0.as_ref().into_enabled()
    }
}

impl ConfigBuilder {
    /// Builds a new `Config` from a builder
    ///
    /// Returns an error unless at least `max` has been specified,
    /// or if the fields values are invalid or inconsistent.
    pub fn build(&self) -> Result<Config, ConfigBuildError> {
        let max = self.max.unwrap_or(Qty::MAX);

        if max == Qty::MAX {
            if self.low_water.is_some() {
                return Err(ConfigBuildError::Inconsistent {
                    fields: vec!["max".into(), "low_water".into()],
                    problem: "low_water supplied, but max omitted".into(),
                });
            };
            return Ok(Config(IfEnabled::Noop));
        }

        let enabled = EnabledToken::new_if_compiled_in()
            //
            .ok_or_else(|| ConfigBuildError::NoCompileTimeSupport {
                field: "max".into(),
                problem: "cargo feature `memquota` disabled (in tor-memquota crate)".into(),
            })?;

        let low_water = self.low_water.unwrap_or_else(
            //
            || Qty((*max as f32 * 0.75) as _),
        );

        let config = ConfigInner { max, low_water };

        /// Minimum low water.  `const` so that overflows are compile-time.
        const MIN_LOW_WATER: usize = crate::mtracker::MAX_CACHE.as_usize() * MIN_MAX_PARTICIPANTS;
        let min_low_water = MIN_LOW_WATER;
        if *config.low_water < min_low_water {
            return Err(ConfigBuildError::Invalid {
                field: "low_water".into(),
                problem: format!("must be at least {min_low_water}"),
            });
        }

        let ratio: f32 = *config.low_water as f32 / *config.max as f32;
        if ratio > MAX_LOW_WATER_RATIO {
            return Err(ConfigBuildError::Inconsistent {
                fields: vec!["low_water".into(), "max".into()],
                problem: format!(
 "low_water / max = {ratio}; must be <= {MAX_LOW_WATER_RATIO}, ideally considerably lower"
                ),
            });
        }

        Ok(Config(IfEnabled::Enabled(config, enabled)))
    }
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->

    use super::*;
    use serde_json::json;

    #[test]
    fn configs() {
        let chk_ok_raw = |j, c| {
            let b: ConfigBuilder = serde_json::from_value(j).unwrap();
            assert_eq!(b.build().unwrap(), c);
        };
        #[cfg(feature = "memquota")]
        let chk_ok = |j, max, low_water| {
            const M: usize = 1024 * 1024;

            let exp = IfEnabled::Enabled(
                ConfigInner {
                    max: Qty(max * M),
                    low_water: Qty(low_water * M),
                },
                EnabledToken::new(),
            );

            chk_ok_raw(j, Config(exp));
        };
        let chk_err = |j, exp| {
            let b: ConfigBuilder = serde_json::from_value(j).unwrap();
            let got = b.build().unwrap_err().to_string();

            #[cfg(not(feature = "memquota"))]
            if got.contains("cargo feature `memquota` disabled") {
                return;
            }

            assert!(got.contains(exp), "in {exp:?} in {got:?}");
        };
        #[cfg(not(feature = "memquota"))]
        let chk_ok = |j, max, low_water| {
            chk_err(j, "UNSUPPORTED");
        };

        chk_ok(json! {{ "max": "8 MiB" }}, 8, 6);
        chk_ok(json! {{ "max": "8 MiB", "low_water": "4 MiB" }}, 8, 4);
        chk_ok_raw(json! {{ }}, Config(IfEnabled::Noop));

        chk_err(
            json! {{ "low_water": "4 MiB" }},
            "low_water supplied, but max omitted",
        );
        chk_err(
            json! {{ "max": "8 MiB", "low_water": "8 MiB" }},
            "inconsistent: low_water / max",
        );
    }
}