1
//! Configuration (private module)
2

            
3
use 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
8
const 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!
13
const MAX_LOW_WATER_RATIO: f32 = 0.98;
14

            
15
define_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
123
        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)]
44
pub 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)]
54
pub 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
)]
77
pub(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

            
89
impl Config {
90
    /// Start building a [`Config`]
91
    ///
92
    /// Returns a fresh default [`ConfigBuilder`].
93
79
    pub fn builder() -> ConfigBuilder {
94
79
        ConfigBuilder::default()
95
79
    }
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
70
    pub fn inner(&self) -> Option<&ConfigInner> {
103
70
        self.0.as_ref().into_enabled()
104
70
    }
105
}
106

            
107
impl 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
2504
    pub fn build(&self) -> Result<Config, ConfigBuildError> {
113
2504
        let max = self.max.unwrap_or(Qty::MAX);
114
2504

            
115
2504
        if max == Qty::MAX {
116
2384
            if self.low_water.is_some() {
117
2
                return Err(ConfigBuildError::Inconsistent {
118
2
                    fields: vec!["max".into(), "low_water".into()],
119
2
                    problem: "low_water supplied, but max omitted".into(),
120
2
                });
121
2382
            };
122
2382
            return Ok(Config(IfEnabled::Noop));
123
120
        }
124

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

            
132
120
        let low_water = self.low_water.unwrap_or_else(
133
120
            //
134
156
            || Qty((*max as f32 * 0.75) as _),
135
120
        );
136
120

            
137
120
        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
120
        let min_low_water = MIN_LOW_WATER;
142
120
        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
120
        }
148
120

            
149
120
        let ratio: f32 = *config.low_water as f32 / *config.max as f32;
150
120
        if ratio > MAX_LOW_WATER_RATIO {
151
2
            return Err(ConfigBuildError::Inconsistent {
152
2
                fields: vec!["low_water".into(), "max".into()],
153
2
                problem: format!(
154
2
 "low_water / max = {ratio}; must be <= {MAX_LOW_WATER_RATIO}, ideally considerably lower"
155
2
                ),
156
2
            });
157
118
        }
158
118

            
159
118
        Ok(Config(IfEnabled::Enabled(config, enabled)))
160
2504
    }
161
}
162

            
163
#[cfg(test)]
164
mod 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
}