1//! Configuration (private module)
23use crate::internal_prelude::*;
45/// 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;
910/// 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;
1415define_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(...)`.
20QtySetters:
2122impl ConfigBuilder {
23 $(
24 ${when approx_equal($ftype, { Option::<Qty> })}
2526 ${fattrs doc}
27///
28 /// (Setter method.)
29pub fn $fname(&mut self, value: usize) -> &mut Self {
30self.$fname = Some(Qty(value));
31self
32}
33 )
34 }
35}
3637/// 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>);
4546/// 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).
62max: Option<Qty>,
6364/// Reclamation will stop when memory use is reduced to below this value
65 ///
66 /// Default is 75% of the maximum.
67low_water: Option<Qty>,
68}
6970/// 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
81pub max: Qty,
8283/// Low water
84 ///
85 /// Guaranteed to be enough lower than `max`
86pub low_water: Qty,
87}
8889impl Config {
90/// Start building a [`Config`]
91 ///
92 /// Returns a fresh default [`ConfigBuilder`].
93pub fn builder() -> ConfigBuilder {
94 ConfigBuilder::default()
95 }
9697/// 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")]
102pub fn inner(&self) -> Option<&ConfigInner> {
103self.0.as_ref().into_enabled()
104 }
105}
106107impl 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.
112pub fn build(&self) -> Result<Config, ConfigBuildError> {
113let max = self.max.unwrap_or(Qty::MAX);
114115if max == Qty::MAX {
116if self.low_water.is_some() {
117return Err(ConfigBuildError::Inconsistent {
118 fields: vec!["max".into(), "low_water".into()],
119 problem: "low_water supplied, but max omitted".into(),
120 });
121 };
122return Ok(Config(IfEnabled::Noop));
123 }
124125let 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 })?;
131132let low_water = self.low_water.unwrap_or_else(
133//
134|| Qty((*max as f32 * 0.75) as _),
135 );
136137let config = ConfigInner { max, low_water };
138139/// Minimum low water. `const` so that overflows are compile-time.
140const MIN_LOW_WATER: usize = crate::mtracker::MAX_CACHE.as_usize() * MIN_MAX_PARTICIPANTS;
141let min_low_water = MIN_LOW_WATER;
142if *config.low_water < min_low_water {
143return Err(ConfigBuildError::Invalid {
144 field: "low_water".into(),
145 problem: format!("must be at least {min_low_water}"),
146 });
147 }
148149let ratio: f32 = *config.low_water as f32 / *config.max as f32;
150if ratio > MAX_LOW_WATER_RATIO {
151return 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 }
158159Ok(Config(IfEnabled::Enabled(config, enabled)))
160 }
161}
162163#[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 @@ -->
178179use super::*;
180use serde_json::json;
181182#[test]
183fn configs() {
184let chk_ok_raw = |j, c| {
185let b: ConfigBuilder = serde_json::from_value(j).unwrap();
186assert_eq!(b.build().unwrap(), c);
187 };
188#[cfg(feature = "memquota")]
189let chk_ok = |j, max, low_water| {
190const M: usize = 1024 * 1024;
191192let exp = IfEnabled::Enabled(
193 ConfigInner {
194 max: Qty(max * M),
195 low_water: Qty(low_water * M),
196 },
197 EnabledToken::new(),
198 );
199200 chk_ok_raw(j, Config(exp));
201 };
202let chk_err = |j, exp| {
203let b: ConfigBuilder = serde_json::from_value(j).unwrap();
204let got = b.build().unwrap_err().to_string();
205206#[cfg(not(feature = "memquota"))]
207if got.contains("cargo feature `memquota` disabled") {
208return;
209 }
210211assert!(got.contains(exp), "in {exp:?} in {got:?}");
212 };
213#[cfg(not(feature = "memquota"))]
214let chk_ok = |j, max, low_water| {
215 chk_err(j, "UNSUPPORTED");
216 };
217218 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));
221222 chk_err(
223json! {{ "low_water": "4 MiB" }},
224"low_water supplied, but max omitted",
225 );
226 chk_err(
227json! {{ "max": "8 MiB", "low_water": "8 MiB" }},
228"inconsistent: low_water / max",
229 );
230 }
231}