//! 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:: })} ${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); /// 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, /// Reclamation will stop when memory use is reduced to below this value /// /// Default is 75% of the maximum. low_water: Option, } /// 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 { 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)] //! 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", ); } }