1
//! Configuration (private module)
2

            
3
use std::sync::LazyLock;
4

            
5
use sysinfo::{MemoryRefreshKind, System};
6
use tracing::warn;
7

            
8
use crate::internal_prelude::*;
9

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

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

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

            
27
    impl ConfigBuilder {
28
      $(
29
        ${when approx_equal($ftype, { Option::<ExplicitOrAuto<Qty>> })}
30

            
31
        ${fattrs doc}
32
        ///
33
        /// (Setter method.)
34
        // We use `value: impl Into<ExplicitOrAuto<usize>>` to avoid breaking users who used the
35
        // previous `value: usize`. But this isn't 100% foolproof, for example if a user used
36
        // `$fname(foo.into())`, which will fail type inference.
37
104
        pub fn $fname(&mut self, value: impl Into<ExplicitOrAuto<usize>>) -> &mut Self {
38
            self.$fname = Some(value.into().map(Qty));
39
            self
40
        }
41
      )
42
    }
43
}
44

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

            
54
/// Configuration for a memory data tracker (builder)
55
//
56
// We could perhaps generate this with `#[derive(Builder)]` on `ConfigInner`,
57
// but derive-builder would need a *lot* of overriding attributes;
58
// and, doing it this way lets us write separate docs about
59
// the invariants on our fields, which are not the same as those in the builder.
60
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default, Deftly)]
61
#[derive_deftly(tor_config::Flattenable, QtySetters)]
62
pub struct ConfigBuilder {
63
    /// Maximum memory usage tolerated before reclamation starts
64
    ///
65
    /// Setting this to `usize::MAX` disables the memory quota.
66
    ///
67
    /// The default is "auto",
68
    /// which uses a value derived from the total system memory.
69
    /// It should not be assumed that the value used for "auto"
70
    /// will remain stable across different versions of this library.
71
    ///
72
    /// Note that this is not a hard limit.
73
    /// See Approximate in [the overview](crate).
74
    max: Option<ExplicitOrAuto<Qty>>,
75

            
76
    /// Reclamation will stop when memory use is reduced to below this value
77
    ///
78
    /// Default is "auto", which uses 75% of the maximum.
79
    /// It should not be assumed that the value used for "auto"
80
    /// will remain stable across different versions of this library.
81
    ///
82
    /// If set to an explicit value,
83
    /// then `max` must be set to an explicit value as well.
84
    low_water: Option<ExplicitOrAuto<Qty>>,
85
}
86

            
87
// NOTE: We derive this manually since the derive_deftly ExtendBuilder macro applies to the
88
// _config_ type. :/
89
impl tor_config::extend_builder::ExtendBuilder for ConfigBuilder {
90
    fn extend_from(&mut self, other: Self, _: tor_config::extend_builder::ExtendStrategy) {
91
        if let Some(max) = other.max {
92
            self.max = Some(max);
93
        }
94
        if let Some(low_water) = other.low_water {
95
            self.low_water = Some(low_water);
96
        }
97
    }
98
}
99

            
100
/// Configuration, if enabled
101
#[derive(Debug, Clone, Eq, PartialEq, Deftly)]
102
#[cfg_attr(
103
    feature = "testing",
104
    visibility::make(pub),
105
    allow(clippy::exhaustive_structs)
106
)]
107
pub(crate) struct ConfigInner {
108
    /// Maximum memory usage
109
    ///
110
    /// Guaranteed not to be `MAX`, since we're enabled
111
    pub max: Qty,
112

            
113
    /// Low water
114
    ///
115
    /// Guaranteed to be enough lower than `max`
116
    pub low_water: Qty,
117
}
118

            
119
impl Config {
120
    /// Start building a [`Config`]
121
    ///
122
    /// Returns a fresh default [`ConfigBuilder`].
123
111
    pub fn builder() -> ConfigBuilder {
124
111
        ConfigBuilder::default()
125
111
    }
126

            
127
    /// Obtain the actual configuration, if we're enabled, or `None` if not
128
    ///
129
    /// Ad-hoc accessor for testing purposes.
130
    /// (ideally we'd use `visibility` to make fields `pub`, but that doesn't work.)
131
    #[cfg(any(test, feature = "testing"))]
132
122
    #[cfg_attr(feature = "testing", visibility::make(pub))]
133
122
    fn inner(&self) -> Option<&ConfigInner> {
134
122
        self.0.as_ref().into_enabled()
135
122
    }
136
}
137

            
138
impl ConfigBuilder {
139
    /// Builds a new `Config` from a builder
140
    ///
141
    /// Returns an error if the fields values are invalid or inconsistent.
142
9280
    pub fn build(&self) -> Result<Config, ConfigBuildError> {
143
        // both options default to "auto"
144
9280
        let max = self.max.unwrap_or(ExplicitOrAuto::Auto);
145
9280
        let low_water = self.low_water.unwrap_or(ExplicitOrAuto::Auto);
146

            
147
        // `MAX` indicates "disabled".
148
        // TODO: Should we add a new "enabled" config option instead of using a sentinel value?
149
        // But this would be a breaking change. Or maybe we should always enable the memquota
150
        // machinery even if the user chooses an unreasonably large value, and not give users a way
151
        // to disable it.
152
9280
        if max == ExplicitOrAuto::Explicit(Qty::MAX) {
153
            // If it should be disabled, but the user provided an explicit value for `low_water`.
154
2
            if matches!(low_water, ExplicitOrAuto::Explicit(_)) {
155
2
                return Err(ConfigBuildError::Inconsistent {
156
2
                    fields: vec!["max".into(), "low_water".into()],
157
2
                    problem: "low_water supplied, but max indicates that we should disable the memory quota".into(),
158
2
                });
159
            };
160
            return Ok(Config(IfEnabled::Noop));
161
9278
        }
162

            
163
        // We don't want the user to set "auto" for `max`, but an explicit value for `low_water`.
164
        // Otherwise this config is prone to breaking since a `max` of "auto" may change as system
165
        // memory is removed (either physically or if running in a VM/container).
166
9278
        if matches!(max, ExplicitOrAuto::Auto) && matches!(low_water, ExplicitOrAuto::Explicit(_)) {
167
2
            return Err(ConfigBuildError::Inconsistent {
168
2
                fields: vec!["max".into(), "low_water".into()],
169
2
                problem: "max is \"auto\", but low_water is set to an explicit quantity".into(),
170
2
            });
171
9276
        }
172

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

            
180
        // The general logic is taken from c-tor (see `compute_real_max_mem_in_queues`).
181
        // NOTE: Relays have an additional lower bound for explicitly given values (64 MiB),
182
        // but we have no way of knowing whether we are a relay or not here.
183
9276
        let max = match max {
184
180
            ExplicitOrAuto::Explicit(x) => x,
185
9096
            ExplicitOrAuto::Auto => compute_max_from_total_system_mem(total_available_memory()),
186
        };
187

            
188
9276
        let low_water = match low_water {
189
113
            ExplicitOrAuto::Explicit(x) => x,
190
9163
            ExplicitOrAuto::Auto => Qty((*max as f32 * 0.75) as _),
191
        };
192

            
193
9276
        let config = ConfigInner { max, low_water };
194

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

            
205
9276
        let ratio: f32 = *config.low_water as f32 / *config.max as f32;
206
9276
        if ratio > MAX_LOW_WATER_RATIO {
207
2
            return Err(ConfigBuildError::Inconsistent {
208
2
                fields: vec!["low_water".into(), "max".into()],
209
2
                problem: format!(
210
2
                    "low_water / max = {ratio}; must be <= {MAX_LOW_WATER_RATIO}, ideally considerably lower"
211
2
                ),
212
2
            });
213
9274
        }
214

            
215
9274
        Ok(Config(IfEnabled::Enabled(config, enabled)))
216
9280
    }
217
}
218

            
219
impl tor_config::load::Builder for ConfigBuilder {
220
    type Built = Config;
221

            
222
    fn build(&self) -> Result<Self::Built, ConfigBuildError> {
223
        ConfigBuilder::build(self)
224
    }
225
}
226

            
227
impl tor_config::load::ConfigBuilder for ConfigBuilder {
228
    fn apply_defaults(&mut self) -> Result<(), ConfigBuildError> {
229
        self.max.get_or_insert_default();
230
        self.low_water.get_or_insert_default();
231
        Ok(())
232
    }
233
}
234

            
235
/// Determine a max given the system's total available memory.
236
///
237
/// This is used when `max` is configured as "auto".
238
/// It takes a `Result` so that we can handle the case where the total memory isn't available.
239
9110
fn compute_max_from_total_system_mem(mem: Result<usize, MemQueryError>) -> Qty {
240
    const MIB: usize = 1024 * 1024;
241
    const GIB: usize = 1024 * 1024 * 1024;
242

            
243
9110
    let mem = match mem {
244
9108
        Ok(x) => x,
245
2
        Err(e) => {
246
2
            warn!("Unable to get the total available memory. Using a constant max instead: {e}");
247

            
248
            // Can't get the total available memory,
249
            // so we return a max depending on whether the architecture is 32-bit or 64-bit.
250
2
            return Qty({
251
2
                cfg_if::cfg_if! {
252
2
                    if #[cfg(target_pointer_width = "64")] {
253
2
                        8 * GIB
254
2
                    } else {
255
2
                        1 * GIB
256
2
                    }
257
2
                }
258
2
            });
259
        }
260
    };
261

            
262
9108
    let mem = {
263
        // From c-tor:
264
        //
265
        // > The idea behind this value is that the amount of RAM is more than enough
266
        // > for a single relay and should allow the relay operator to run two relays
267
        // > if they have additional bandwidth available.
268
9108
        let mut factor = 0.75;
269
        // Multiplying 8 * GIB overflows the usize limit (4 GIB - 1) on 32-bit
270
        // platforms. So handle this properly for 32-bit platforms. Memory on 32-bit
271
        // targets cannot exceed 4 GIB anyways.
272
        #[cfg(target_pointer_width = "64")]
273
9108
        if mem >= 8 * GIB {
274
9100
            factor = 0.40;
275
9100
        }
276
9108
        (mem as f64 * factor) as usize
277
    };
278

            
279
    // The (min, max) range to clamp `mem` to.
280
9108
    let clamp = {
281
        cfg_if::cfg_if! {
282
            if #[cfg(target_pointer_width = "64")] {
283
9108
                (256 * MIB, 8 * GIB)
284
            } else {
285
                (256 * MIB, 2 * GIB)
286
            }
287
        }
288
    };
289

            
290
9108
    let mem = mem.clamp(clamp.0, clamp.1);
291

            
292
9108
    Qty(mem)
293
9110
}
294

            
295
/// The total available memory in bytes.
296
///
297
/// This is generally the amount of system RAM,
298
/// but we may also take into account other OS-specific limits such as cgroups.
299
///
300
/// Returns `None` if we were unable to get the total available memory.
301
/// But see internal comments for details.
302
9096
fn total_available_memory() -> Result<usize, MemQueryError> {
303
    // The sysinfo crate says we should use only one `System` per application.
304
    // But we're a library, so it's probably best to just make this global and reuse it.
305
    // In reality getting the system memory probably shouldn't require persistent state,
306
    // but since the internals of the sysinfo crate are opaque to us,
307
    // we'll just follow their documentation and cache the `System`.
308
    //
309
    // NOTE: The sysinfo crate in practice gets more information than we ask for.
310
    // For example `System::new()` will always query the `_SC_PAGESIZE` and `_SC_CLK_TCK`
311
    // on Linux even though we only refresh the memory info below
312
    // (see https://github.com/GuillaumeGomez/sysinfo/blob/fc31b411eea7b9983176399dc5be162786dec95b/src/unix/linux/system.rs#L152).
313
    // This means that miri will fail to run on tests that build the config, even if the config uses
314
    // explicit values.
315
3247
    static SYSTEM: LazyLock<Mutex<System>> = LazyLock::new(|| Mutex::new(System::new()));
316
9096
    let mut system = SYSTEM.lock().unwrap_or_else(|mut e| {
317
        // The sysinfo crate has some internal panics which would poison this mutex.
318
        // But we can easily reset it, rather than panicking ourselves if it's poisoned.
319
        **e.get_mut() = System::new();
320
        SYSTEM.clear_poison();
321
        e.into_inner()
322
    });
323

            
324
9096
    system.refresh_memory_specifics(MemoryRefreshKind::nothing().with_ram());
325

            
326
    // It might be possible for 32-bit systems to return >usize::MAX due to PAE (I haven't looked
327
    // into this), so we just saturate the value and don't consider this an error.
328
9096
    let mem = to_usize_saturating(system.total_memory());
329

            
330
    // The sysinfo crate doesn't report errors, so the best we can do is guess that a value of 0
331
    // implies that it was unable to get the total memory.
332
    //
333
    // We also need to return early to prevent a panic below.
334
9096
    if mem == 0 {
335
        return Err(MemQueryError::Unavailable);
336
9096
    }
337

            
338
    // Note: The docs for the sysinfo crate say:
339
    //
340
    // > You need to have run refresh_memory at least once before calling this method.
341
    //
342
    // But as implemented, it also panics if `sys.mem_total == 0` (for example if the refresh
343
    // silently failed).
344
9096
    let Some(cgroups) = system.cgroup_limits() else {
345
        // There is no cgroup (or we're a non-Linux platform).
346
        return Ok(mem);
347
    };
348

            
349
    // The `cgroup_limits()` surprisingly doesn't actually return the unaltered cgroups limits.
350
    // It also adjusts them depending on the total memory.
351
    // Since this is all undocumented, we'll also do the same calculation here.
352
9096
    let mem = std::cmp::min(mem, to_usize_saturating(cgroups.total_memory));
353

            
354
9096
    Ok(mem)
355
9096
}
356

            
357
/// An error when we are unable to obtain the system's total available memory.
358
#[derive(Clone, Debug, thiserror::Error)]
359
enum MemQueryError {
360
    /// The total available memory is unavailable.
361
    #[error("total available memory is unavailable")]
362
    Unavailable,
363
}
364

            
365
/// Convert a `u64` to a `usize`, saturating if the value would overflow.
366
18192
fn to_usize_saturating(x: u64) -> usize {
367
    // this will be optimized to a no-op on 64-bit systems
368
18192
    x.try_into().unwrap_or(usize::MAX)
369
18192
}
370

            
371
#[cfg(test)]
372
mod test {
373
    // @@ begin test lint list maintained by maint/add_warning @@
374
    #![allow(clippy::bool_assert_comparison)]
375
    #![allow(clippy::clone_on_copy)]
376
    #![allow(clippy::dbg_macro)]
377
    #![allow(clippy::mixed_attributes_style)]
378
    #![allow(clippy::print_stderr)]
379
    #![allow(clippy::print_stdout)]
380
    #![allow(clippy::single_char_pattern)]
381
    #![allow(clippy::unwrap_used)]
382
    #![allow(clippy::unchecked_time_subtraction)]
383
    #![allow(clippy::useless_vec)]
384
    #![allow(clippy::needless_pass_by_value)]
385
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
386

            
387
    use super::*;
388
    use serde_json::json;
389

            
390
    #[test]
391
    // A value of "auto" depends on the system memory,
392
    // which typically results in libc calls or syscall that aren't supported by miri.
393
    #[cfg_attr(miri, ignore)]
394
    fn configs() {
395
        let chk_ok_raw = |j, c| {
396
            let b: ConfigBuilder = serde_json::from_value(j).unwrap();
397
            assert_eq!(b.build().unwrap(), c);
398
        };
399
        #[cfg(feature = "memquota")]
400
        let chk_ok = |j, max, low_water| {
401
            const M: usize = 1024 * 1024;
402

            
403
            let exp = IfEnabled::Enabled(
404
                ConfigInner {
405
                    max: Qty(max * M),
406
                    low_water: Qty(low_water * M),
407
                },
408
                EnabledToken::new(),
409
            );
410

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

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

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

            
429
        let chk_builds = |j| {
430
            cfg_if::cfg_if! {
431
                if #[cfg(feature = "memquota")] {
432
                    let b: ConfigBuilder = serde_json::from_value(j).unwrap();
433
                    b.build().unwrap();
434
                } else {
435
                    chk_err(j, "UNSUPPORTED");
436
                }
437
            }
438
        };
439

            
440
        chk_ok(json! {{ "max": "8 MiB" }}, 8, 6);
441
        chk_ok(json! {{ "max": "8 MiB", "low_water": "auto" }}, 8, 6);
442
        chk_ok(json! {{ "max": "8 MiB", "low_water": "4 MiB" }}, 8, 4);
443

            
444
        // We don't know what the exact values will be since they are derived from the system
445
        // memory.
446
        chk_builds(json! {{ }});
447
        chk_builds(json! {{ "max": "auto" }});
448
        chk_builds(json! {{ "low_water": "auto" }});
449
        chk_builds(json! {{ "max": "auto", "low_water": "auto" }});
450

            
451
        chk_err(
452
            json! {{ "low_water": "4 MiB" }},
453
            "max is \"auto\", but low_water is set to an explicit quantity",
454
        );
455
        chk_err(
456
            json! {{ "max": "8 MiB", "low_water": "8 MiB" }},
457
            "inconsistent: low_water / max",
458
        );
459

            
460
        // `usize::MAX` is a special value.
461
        chk_err(
462
            json! {{ "max": usize::MAX.to_string(), "low_water": "8 MiB" }},
463
            "low_water supplied, but max indicates that we should disable the memory quota",
464
        );
465
        chk_builds(json! {{ "max": (usize::MAX - 1).to_string(), "low_water": "8 MiB" }});
466

            
467
        // check that the builder works as expected
468
        #[cfg(feature = "memquota")]
469
        {
470
            let mut b = Config::builder();
471
            b.max(ExplicitOrAuto::Explicit(100_000_000));
472
            if let Some(inner) = b.build().unwrap().inner() {
473
                assert_eq!(inner.max, Qty(100_000_000));
474
            }
475

            
476
            let mut b = Config::builder();
477
            b.max(100_000_000);
478
            if let Some(inner) = b.build().unwrap().inner() {
479
                assert_eq!(inner.max, Qty(100_000_000));
480
            }
481

            
482
            let mut b = ConfigBuilder::default();
483
            b.max(ExplicitOrAuto::Auto);
484
            b.build().unwrap();
485
        }
486
    }
487

            
488
    /// Test the logic that computes the `max` when configured as "auto".
489
    #[test]
490
    // We do some `1 * X` operations below for readability.
491
    #[allow(clippy::identity_op)]
492
    fn auto_max() {
493
        #[allow(unused)]
494
        fn check_helper(val: Qty, expected_32: Qty, expected_64: Qty) {
495
            assert_eq!(val, {
496
                cfg_if::cfg_if! {
497
                    if #[cfg(target_pointer_width = "64")] {
498
                        expected_64
499
                    } else if #[cfg(target_pointer_width = "32")] {
500
                        expected_32
501
                    } else {
502
                        panic!("Unsupported architecture :(");
503
                    }
504
                }
505
            });
506
        }
507

            
508
        check_helper(
509
            compute_max_from_total_system_mem(Err(MemQueryError::Unavailable)),
510
            /* 32-bit */ Qty(1 * 1024 * 1024 * 1024),
511
            /* 64-bit */ Qty(8 * 1024 * 1024 * 1024),
512
        );
513
        check_helper(
514
            compute_max_from_total_system_mem(Ok(8 * 1024 * 1024 * 1024)),
515
            /* 32-bit */ Qty(2 * 1024 * 1024 * 1024),
516
            /* 64-bit */ Qty(3435973836),
517
        );
518
        check_helper(
519
            compute_max_from_total_system_mem(Ok(7 * 1024 * 1024 * 1024)),
520
            /* 32-bit */ Qty(2 * 1024 * 1024 * 1024),
521
            /* 64-bit */ Qty(5637144576),
522
        );
523
        check_helper(
524
            compute_max_from_total_system_mem(Ok(1 * 1024 * 1024 * 1024)),
525
            /* 32-bit */ Qty(805306368),
526
            /* 64-bit */ Qty(805306368),
527
        );
528
        check_helper(
529
            compute_max_from_total_system_mem(Ok(7 * 1024)),
530
            /* 32-bit */ Qty(256 * 1024 * 1024),
531
            /* 64-bit */ Qty(256 * 1024 * 1024),
532
        );
533
        check_helper(
534
            compute_max_from_total_system_mem(Ok(0)),
535
            /* 32-bit */ Qty(256 * 1024 * 1024),
536
            /* 64-bit */ Qty(256 * 1024 * 1024),
537
        );
538
        check_helper(
539
            compute_max_from_total_system_mem(Ok(usize::MAX)),
540
            /* 32-bit */ Qty(2 * 1024 * 1024 * 1024),
541
            /* 64-bit */ Qty(8 * 1024 * 1024 * 1024),
542
        );
543
    }
544
}