summaryrefslogtreecommitdiff
path: root/crates/tor-memquota/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/tor-memquota/src')
-rw-r--r--crates/tor-memquota/src/config.rs103
-rw-r--r--crates/tor-memquota/src/drop_bomb.rs515
-rw-r--r--crates/tor-memquota/src/drop_reentrancy.rs143
-rw-r--r--crates/tor-memquota/src/error.rs144
-rw-r--r--crates/tor-memquota/src/internal_prelude.rs63
-rw-r--r--crates/tor-memquota/src/lib.rs206
-rw-r--r--crates/tor-memquota/src/mtracker.rs1047
-rw-r--r--crates/tor-memquota/src/mtracker/bookkeeping.rs308
-rw-r--r--crates/tor-memquota/src/mtracker/reclaim.rs396
-rw-r--r--crates/tor-memquota/src/mtracker/reclaim/deferred_drop.rs81
-rw-r--r--crates/tor-memquota/src/mtracker/test.rs678
-rw-r--r--crates/tor-memquota/src/mtracker/total_qty_notifier.rs70
-rw-r--r--crates/tor-memquota/src/refcount.rs315
-rw-r--r--crates/tor-memquota/src/utils.rs69
14 files changed, 4138 insertions, 0 deletions
diff --git a/crates/tor-memquota/src/config.rs b/crates/tor-memquota/src/config.rs
new file mode 100644
index 000000000..ce8fd364c
--- /dev/null
+++ b/crates/tor-memquota/src/config.rs
@@ -0,0 +1,103 @@
+//! 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 $< $ttype Builder > {
+ $(
+ ${when approx_equal($ftype, 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.
+#[derive(Debug, Clone, Builder, Eq, PartialEq, Deftly)]
+#[derive_deftly(QtySetters)]
+#[builder(build_fn(private, name = "build_unvalidated", error = "ConfigBuildError"))]
+#[builder(derive(Serialize, Deserialize, Debug, Deftly, Eq, PartialEq))]
+#[builder_struct_attr(derive_deftly(tor_config::Flattenable))]
+pub struct Config {
+ /// Maximum memory usage tolerated before reclamation starts
+ ///
+ /// Note that this is not a hard limit.
+ /// See Approximate in [the overview](crate).
+ ///
+ ///
+ #[builder(setter(custom))]
+ pub(crate) max: Qty,
+
+ /// Reclamation will stop when memory use is reduced to below this value
+ ///
+ /// Default is 75% of the maximum.
+ #[builder(setter(custom))]
+ pub(crate) low_water: Qty,
+}
+
+impl Config {
+ /// Start building a [`Config`]
+ ///
+ /// Returns a fresh default [`ConfigBuilder`].
+ pub fn builder() -> ConfigBuilder {
+ ConfigBuilder::default()
+ }
+}
+
+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 mut builder = self.clone();
+ if let (Some(max), None) = (builder.max, builder.low_water) {
+ builder.low_water = Some(Qty((*max as f32 * 0.75) as _));
+ }
+ let config = builder.build_unvalidated()?;
+
+ let min_low_water = crate::mtracker::MAX_CACHE.as_usize() * MIN_MAX_PARTICIPANTS;
+ 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)
+ }
+}
diff --git a/crates/tor-memquota/src/drop_bomb.rs b/crates/tor-memquota/src/drop_bomb.rs
new file mode 100644
index 000000000..6a7b55fa6
--- /dev/null
+++ b/crates/tor-memquota/src/drop_bomb.rs
@@ -0,0 +1,515 @@
+//! Drop bombs, for assurance of postconditions when types are dropped
+//!
+//! Provides two drop bomb types: [`DropBomb`] and [`DropBombCondition`].
+//!
+//! These help assure that our algorithms are correct,
+//! by detecting when types that contain the bomb are dropped inappropriately.
+//!
+//! # No-op outside `#[cfg(test)]`
+//!
+//! When used outside test code, these types are unit ZSTs,
+//! and are completely inert.
+//! They won't cause panics or detect bugs, in production.
+//!
+//! # Panics (in tests), and simulation
+//!
+//! These types work by panicking in drop, when a bug is detected.
+//! This will then cause a test failure.
+//! Such panics are described as "explodes (panics)" in the documentation.
+//!
+//! There are also simulated drop bombs, whose explosions do not actually panic.
+//! Instead, they record that a panic would have occurred,
+//! and print a message to stderr.
+//! The constructors provide a handle to allow the caller to enquire about explosions.
+//! This allows for testing a containing type's drop bomb logic.
+//!
+//! Certain misuses result in actual panics, even with simulated bombs.
+//! This is described as "panics (actually)".
+//!
+//! # Choosing a bomb
+//!
+//! [`DropBomb`] is for assuring the runtime context or appropriate timing of drops
+//! (and could be used for implementing general conditions).
+//!
+//! [`DropBombCondition`] is for assuring the properties of a value that is being dropped.
+
+use crate::internal_prelude::*;
+
+#[cfg(test)]
+use std::sync::atomic::{AtomicBool, Ordering};
+
+//---------- macros used in this module, and supporting trait ----------
+
+define_derive_deftly! {
+ /// Helper for common impls on bombs
+ ///
+ /// * Provides `fn new_armed`
+ /// * Provides `fn new_simulated`
+ /// * Implements `Drop`, using `TestableDrop::drop_impl`
+ BombImpls =
+
+ impl $ttype {
+ /// Create a new drop bomb, which must be properly disposed of
+ pub(crate) const fn new_armed() -> Self {
+ let status = Status::ARMED_IN_TESTS;
+ $ttype { status }
+ }
+ }
+
+ #[cfg(test)]
+ impl $ttype {
+ /// Create a simulated drop bomb
+ pub(crate) fn new_simulated() -> (Self, SimulationHandle) {
+ let handle = SimulationHandle::new();
+ let status = S::ArmedSimulated(handle.clone());
+ ($ttype { status }, handle)
+ }
+
+ /// Turn an existing armed drop bomb into a simulated one
+ ///
+ /// This is useful for writing test cases, without having to make a `new_simulated`
+ /// constructor for whatever type contains the drop bomb.
+ /// Instead, construct it normally, and then reach in and call this on the bomb.
+ ///
+ /// # Panics
+ ///
+ /// `self` must be armed. Otherwise, (actually) panics.
+ pub(crate) fn make_simulated(&mut self) -> SimulationHandle {
+ let handle = SimulationHandle::new();
+ let new_status = S::ArmedSimulated(handle.clone());
+ let old_status = mem::replace(&mut self.status, new_status);
+ assert!(matches!(old_status, S::Armed));
+ handle
+ }
+
+ /// Implemnetation of `Drop::drop`, split out for testability.
+ ///
+ /// Calls `drop_status`, and replaces `self.status` with `S::Disarmed`,
+ /// so that `self` can be actually dropped (if we didn't panic).
+ fn drop_impl(&mut self) {
+ // Do the replacement first, so that if drop_status unwinds, we don't panic in panic.
+ let status = mem::replace(&mut self.status, S::Disarmed);
+ <$ttype as DropStatus>::drop_status(status);
+ }
+ }
+
+
+ #[cfg(test)]
+ impl Drop for $ttype {
+ fn drop(&mut self) {
+ // We don't check for unwinding.
+ // We shouldn't drop a nonzero one of these even if we're panicking.
+ // If we do, it'll be a double panic => abort.
+ self.drop_impl();
+ }
+ }
+}
+
+/// Core of `Drop`, that can be called separately, for testing
+///
+/// To use: implement this, and derive deftly
+/// [`BombImpls`](derive_deftly_template_BombImpls).
+trait DropStatus {
+ /// Handles dropping of a `Self` with this `status` field value
+ fn drop_status(status: Status);
+}
+
+//---------- public types ----------
+
+/// Drop bomb: for assuring that drops happen only when expected
+///
+/// Obtained from [`DropBomb::new_armed()`].
+///
+/// # Explosions
+///
+/// Explodes (panicking) if dropped,
+/// unless [`.disarm()`](DropBomb::disarm) is called first.
+#[derive(Deftly, Debug)]
+#[derive_deftly(BombImpls)]
+pub(crate) struct DropBomb {
+ /// What state are we in
+ status: Status,
+}
+
+/// Drop condition: for ensuring that a condition is true, on drop
+///
+/// Obtained from [`DropBombCondition::new_armed()`].
+///
+/// Instead of dropping this, you must call
+/// `drop_bomb_disarm_assert!`
+/// (or its internal function `disarm_assert()`.
+// rustdoc can't manage to make a link to this crate-private macro or cfg-test item.
+///
+/// It will often be necessary to add `#[allow(dead_code)]`
+/// on the `DropBombCondition` field of a containing type,
+/// since outside tests, the `Drop` impl will usually be configured out,
+/// and that's the only place this field is actually read.
+///
+/// # Panics
+///
+/// Panics (actually) if it is simply dropped.
+#[derive(Deftly, Debug)]
+#[derive_deftly(BombImpls)]
+pub(crate) struct DropBombCondition {
+ /// What state are we in
+ #[allow(dead_code)] // not read outside tests
+ status: Status,
+}
+
+/// Handle onto a simulated [`DropBomb`] or [`DropCondition`]
+///
+/// Can be used to tell whether the bomb "exploded"
+/// (ie, whether `drop` would have panicked, if this had been a non-simulated bomb).
+#[cfg(test)]
+#[derive(Debug)]
+pub(crate) struct SimulationHandle {
+ exploded: Arc<AtomicBool>,
+}
+
+/// Unit token indicating that a simulated drop bomb did explode, and would have panicked
+#[cfg(test)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub(crate) struct SimulationExploded;
+
+//---------- internal types ----------
+
+/// State of some kind of drop bomb
+///
+/// This type is inert; the caller is responsible for exploding or panicking.
+#[derive(Debug)]
+enum Status {
+ /// This bomb is disarmed and will not panic.
+ ///
+ /// This is always the case outside `#[cfg(test)]`
+ Disarmed,
+
+ /// This bomb is armed. It will (or may) panic on drop.
+ #[cfg(test)]
+ Armed,
+
+ /// This bomb is armed, but we're running in simulation.
+ #[cfg(test)]
+ ArmedSimulated(SimulationHandle),
+}
+
+use Status as S;
+
+//---------- DropBomb impls ----------
+
+impl DropBomb {
+ /// Disarm this bomb.
+ ///
+ /// It will no longer explode (panic) when dropped.
+ pub(crate) fn disarm(&mut self) {
+ self.status = S::Disarmed;
+ }
+}
+
+#[cfg(test)]
+impl DropStatus for DropBomb {
+ fn drop_status(status: Status) {
+ match status {
+ S::Disarmed => {}
+ S::Armed => panic!("DropBomb dropped without a previous call to .disarm()"),
+ S::ArmedSimulated(handle) => handle.set_exploded(),
+ }
+ }
+}
+
+//---------- DropCondition impls ----------
+
+/// Check the condition, and disarm the bomb
+///
+/// If `CONDITION` is true, disarms the bomb; otherwise, explodes (panics).
+///
+/// # Syntax
+///
+/// ```
+/// drop_bomb_disarm_assert!(BOMB, CONDITION);
+/// drop_bomb_disarm_assert!(BOMB, CONDITION, "FORMAT", FORMAT_ARGS..);
+/// ```
+///
+/// where
+///
+/// * `BOMB: &mut DropCondition` (or something that derefs to that).
+/// * `CONDITION: bool`
+///
+/// # Example
+///
+/// ```
+/// # struct S { drop_bomb: DropCondition };
+/// # impl S { fn f(&mut self) {
+/// drop_bomb_disarm_assert!(self.drop_bomb, self.raw, Qty(0));
+/// # } }
+/// ```
+///
+/// # Explodes
+///
+/// Explodes unless the condition is satisfied.
+//
+// This macro has this long name because we can't do scoping of macro-rules macros.
+#[cfg(test)] // Should not be used outside tests, since the drop impls should be conditional
+macro_rules! drop_bomb_disarm_assert {
+ { $bomb:expr, $condition:expr $(,)? } => {
+ $bomb.disarm_assert(
+ || $condition,
+ format_args!(concat!("condition = ", stringify!($condition))),
+ )
+ };
+ { $bomb:expr, $condition:expr, $fmt:literal $($rest:tt)* } => {
+ $bomb.disarm_assert(
+ || $condition,
+ format_args!(concat!("condition = ", stringify!($condition), ": ", $fmt),
+ $($rest)*),
+ )
+ };
+}
+
+impl DropBombCondition {
+ /// Check a condition, and disarm the bomb
+ ///
+ /// If `call()` returns true, disarms the bomb; otherwise, explodes (panics).
+ ///
+ /// # Explodes
+ ///
+ /// Explodes unless the condition is satisfied.
+ #[inline]
+ #[cfg(test)] // Should not be used outside tests, since the drop impls should be conditional
+ pub(crate) fn disarm_assert(&mut self, call: impl FnOnce() -> bool, msg: fmt::Arguments) {
+ match mem::replace(&mut self.status, S::Disarmed) {
+ S::Disarmed => {
+ // outside cfg(test), this is the usual path.
+ // placate the compiler: we ignore all our arguments
+ let _ = call;
+ let _ = msg;
+
+ #[cfg(test)]
+ panic!("disarm_assert called more than once!");
+ }
+ #[cfg(test)]
+ S::Armed => {
+ if !call() {
+ panic!("drop condition violated: dropped, but condition is false: {msg}");
+ }
+ }
+ #[cfg(test)]
+ #[allow(clippy::print_stderr)]
+ S::ArmedSimulated(handle) => {
+ if !call() {
+ eprintln!("drop condition violated in simulation: {msg}");
+ handle.set_exploded();
+ }
+ }
+ }
+ }
+}
+
+/// Ideally, if you use this, your struct's other default values meet your drop condition!
+impl Default for DropBombCondition {
+ fn default() -> DropBombCondition {
+ Self::new_armed()
+ }
+}
+
+#[cfg(test)]
+impl DropStatus for DropBombCondition {
+ fn drop_status(status: Status) {
+ assert!(matches!(status, S::Disarmed));
+ }
+}
+
+//---------- SimulationHandle impls ----------
+
+#[cfg(test)]
+impl SimulationHandle {
+ /// Determine whether a drop bomb would have been triggered
+ ///
+ /// If the corresponding [`DropBomb]` or [`DropCondition`]
+ /// would have panicked (if we weren't simulating),
+ /// returns `Err`.
+ ///
+ /// # Panics
+ ///
+ /// The corresponding `DropBomb` or `DropCondition` must have been dropped.
+ /// Otherwise, calling `outcome` will (actually) panic.
+ pub(crate) fn outcome(mut self) -> Result<(), SimulationExploded> {
+ let panicked = Arc::into_inner(mem::take(&mut self.exploded))
+ .expect("bomb has not yet been dropped")
+ .into_inner();
+ if panicked {
+ Err(SimulationExploded)
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Require that this bomb did *not* explode
+ ///
+ /// # Panics
+ ///
+ /// Panics if corresponding `DropBomb` hasn't yet been dropped,
+ /// or if it exploded when it was dropped.
+ pub(crate) fn expect_ok(self) {
+ let () = self.outcome().expect("bomb unexpectedly exploded");
+ }
+
+ /// Require that this bomb *did* explode
+ ///
+ /// # Panics
+ ///
+ /// Panics if corresponding `DropBomb` hasn't yet been dropped,
+ /// or if it did *not* explode when it was dropped.
+ pub(crate) fn expect_exploded(self) {
+ let SimulationExploded = self
+ .outcome()
+ .expect_err("bomb unexpectedly didn't explode");
+ }
+
+ /// Return a new handle with no explosion recorded
+ fn new() -> Self {
+ SimulationHandle {
+ exploded: Default::default(),
+ }
+ }
+
+ /// Return a clone of this handle
+ //
+ // Deliberately not a public Clone impl
+ fn clone(&self) -> Self {
+ SimulationHandle {
+ exploded: self.exploded.clone(),
+ }
+ }
+
+ /// Mark this simulated bomb as having exploded
+ fn set_exploded(&self) {
+ self.exploded.store(true, Ordering::Release);
+ }
+}
+
+//---------- internal impls ----------
+
+impl Status {
+ /// Armed, in tests
+ #[cfg(test)]
+ const ARMED_IN_TESTS: Status = S::Armed;
+
+ /// "Armed", outside tests, is in fact not armed
+ #[cfg(not(test))]
+ const ARMED_IN_TESTS: Status = S::Disarmed;
+}
+
+#[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 @@ -->
+ #![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
+
+ use super::*;
+ use std::any::Any;
+ use std::panic::catch_unwind;
+
+ #[test]
+ fn bomb_disarmed() {
+ let mut b = DropBomb::new_armed();
+ b.disarm();
+ drop(b);
+ }
+
+ #[test]
+ fn bomb_panic() {
+ let mut b = DropBomb::new_armed();
+ let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| b.drop_impl())).unwrap_err();
+ }
+
+ #[test]
+ fn bomb_sim_disarmed() {
+ let (mut b, h) = DropBomb::new_simulated();
+ b.disarm();
+ drop(b);
+ h.expect_ok();
+ }
+
+ #[test]
+ fn bomb_sim_explosion() {
+ let (b, h) = DropBomb::new_simulated();
+ drop(b);
+ h.expect_exploded();
+ }
+
+ #[test]
+ fn bomb_make_sim_explosion() {
+ let mut b = DropBomb::new_armed();
+ let h = b.make_simulated();
+ drop(b);
+ h.expect_exploded();
+ }
+
+ struct HasBomb {
+ on_drop: Result<(), ()>,
+ bomb: DropBombCondition,
+ }
+
+ impl Drop for HasBomb {
+ fn drop(&mut self) {
+ drop_bomb_disarm_assert!(self.bomb, self.on_drop.is_ok());
+ }
+ }
+
+ #[test]
+ fn cond_ok() {
+ let hb = HasBomb {
+ on_drop: Ok(()),
+ bomb: DropBombCondition::new_armed(),
+ };
+ drop(hb);
+ }
+
+ #[test]
+ fn cond_sim_explosion() {
+ let (bomb, h) = DropBombCondition::new_simulated();
+ let hb = HasBomb {
+ on_drop: Err(()),
+ bomb,
+ };
+ drop(hb);
+ h.expect_exploded();
+ }
+
+ #[test]
+ fn cond_explosion_panic() {
+ // make an actual panic
+ let mut bomb = DropBombCondition::new_armed();
+ let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| {
+ bomb.disarm_assert(|| false, format_args!("testing"));
+ }))
+ .unwrap_err();
+ }
+
+ #[test]
+ fn cond_forgot_drop_impl() {
+ // pretend that we put a DropBombCondition on this,
+ // but we forgot to impl Drop and call drop_bomb_disarm_assert
+ struct ForgotDropImpl {
+ bomb: DropBombCondition,
+ }
+ let fdi = ForgotDropImpl {
+ bomb: DropBombCondition::new_armed(),
+ };
+ // pretend that fdi is being dropped
+ let mut bomb = fdi.bomb; // move out
+
+ let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| bomb.drop_impl())).unwrap_err();
+ }
+}
diff --git a/crates/tor-memquota/src/drop_reentrancy.rs b/crates/tor-memquota/src/drop_reentrancy.rs
new file mode 100644
index 000000000..3e157cf88
--- /dev/null
+++ b/crates/tor-memquota/src/drop_reentrancy.rs
@@ -0,0 +1,143 @@
+//! Newtype which helps assure lack of drop entrance hazards
+//!
+//! Provides a drop bomb which will help tests detect latent bugs.
+//!
+//! We want this because there are places where we handle an Arc containing
+//! a user-provided trait object, but where we want to prevent invoking
+//! the user's Drop impl since that may lead to reentrancy.
+//!
+//! See the section on "Reentrancy" in the docs for `mtracker::State`.
+//!
+//! Outside tests, the types in this module are equivalent to `std::sync`'s.
+//! So, we never panic in a drop in production.
+//! Dropping in the wrong place might lead to a deadlock
+//! (due to mutex reentrancy)
+//! but this is far from certain:
+//! probably, while we're running, the caller has another live reference,
+//! so the drop of the underlying type won't happen now anyway.
+//!
+//! In any case, drop bombs mustn't be used in production.
+//! Not only can they escalate the severity of problems,
+//! where the program might blunder on,
+//! but also
+//! because Rust upstream are seriously considering
+//! [turning them into aborts](https://github.com/rust-lang/rfcs/pull/3288)!
+//
+// There are no separate tests for this module. Drop bombs are hard to test for.
+// However, in an ad-hoc test, the bomb has been shown to be able to explode,
+// if a `ProtectedArc` is dropped.
+
+use crate::internal_prelude::*;
+
+/// A `Weak<P>`, but upgradeable only to a `ProtectedArc`, not a raw `Arc`.
+#[derive(Debug)]
+pub(crate) struct ProtectedWeak<P: ?Sized>(Weak<P>);
+
+/// An `Arc`, but containing a type which should only be dropped in certain places
+///
+/// In non `#[cfg(test)]` builds, this is just `Arc<P>`.
+///
+/// When testing, it has a drop bomb. You must call `.promise_dropping_is_ok`.
+/// It will panic if it's simply dropped.
+#[derive(Debug, Deref, DerefMut)]
+pub(crate) struct ProtectedArc<P: ?Sized> {
+ /// The actual explosive (might be armed or disarmed)
+ bomb: DropBomb,
+
+ /// The underlying `Arc`
+ #[deref(forward)]
+ #[deref_mut(forward)]
+ arc: Arc<P>,
+}
+
+impl<P: ?Sized> ProtectedWeak<P> {
+ /// Make a new `ProtectedWeak`
+ pub(crate) fn new(p: Weak<P>) -> Self {
+ ProtectedWeak(p)
+ }
+
+ /// Upgrade a `ProtectedWeak` to a `ProtectedArc`, if it's not been garbage collected
+ pub(crate) fn upgrade(&self) -> Option<ProtectedArc<P>> {
+ Some(ProtectedArc::new(self.0.upgrade()?))
+ }
+
+ /// Convert back into an unprotected `Weak`.
+ ///
+ /// # CORRECTNESS
+ ///
+ /// You must arrange that the drop reentrancy requirements aren't violated
+ /// by `Arc`s made from the returned `Weak`.
+ pub(crate) fn unprotect(self) -> Weak<P> {
+ self.0
+ }
+}
+
+impl<P: ?Sized> ProtectedArc<P> {
+ /// Make a new `ProtectedArc` from a raw `Arc`
+ ///
+ /// # CORRECTNESS
+ ///
+ /// Presumably the `Arc` came from an uncontrolled external source, such as user code.
+ pub(crate) fn new(arc: Arc<P>) -> Self {
+ let bomb = DropBomb::new_armed();
+ ProtectedArc { arc, bomb }
+ }
+
+ /// Obtain a `ProtectedWeak` from a `&ProtectedArc`
+ //
+ // If this were a more general-purpose library, we'd avoid this and other methods on `self`.
+ pub(crate) fn downgrade(&self) -> ProtectedWeak<P> {
+ ProtectedWeak(Arc::downgrade(&self.arc))
+ }
+
+ /// Convert back into an unprotected `Arc`
+ ///
+ /// # CORRECTNESS
+ ///
+ /// If the return value is dropped, the location must be suitable for that.
+ /// Or, maybe the returned value is going to calling code in the external user,
+ /// (which, therefore, wouldn't pose a reentrancy hazard).
+ pub(crate) fn promise_dropping_is_ok(mut self) -> Arc<P> {
+ self.bomb.disarm();
+ self.arc
+ }
+}
+
+#[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 @@ -->
+ #![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
+
+ use super::*;
+
+ struct Payload;
+
+ #[test]
+ fn fine() {
+ let arc = Arc::new(Payload);
+ let prot = ProtectedArc::new(arc);
+ let arc = prot.promise_dropping_is_ok();
+ drop(arc);
+ }
+
+ #[test]
+ fn bad() {
+ let arc = Arc::new(Payload);
+ let mut prot = ProtectedArc::new(arc);
+ let h = prot.bomb.make_simulated();
+ drop(prot);
+ h.expect_exploded();
+ }
+}
diff --git a/crates/tor-memquota/src/error.rs b/crates/tor-memquota/src/error.rs
new file mode 100644
index 000000000..4cca5bb10
--- /dev/null
+++ b/crates/tor-memquota/src/error.rs
@@ -0,0 +1,144 @@
+//! Errors arising from memory tracking
+
+use crate::internal_prelude::*;
+
+/// An error occurring when tracking memory usage
+#[derive(Debug, Clone, Error)]
+#[non_exhaustive]
+pub enum Error {
+ /// The memory quota tracker has been torn down
+ #[error("attempted to use shut down memory tracker")]
+ TrackerShutdown,
+
+ /// The Account has been torn down
+ ///
+ /// This can happen if the account or participant has Collapsed due to reclamation
+ #[error("memory pressure (attempted to use closed memory tracking account)")]
+ AccountClosed,
+
+ /// The Participant has been torn down
+ ///
+ /// This can happen if the account or participant has Collapsed due to reclamation
+ #[error("memory pressure (attempt to allocate by torn-down memory tracking participant)")]
+ ParticipantShutdown,
+
+ /// Previous bug, memory quota tracker is corrupted
+ #[error("memory tracker is corrupted due to previous bug")]
+ TrackerCorrupted,
+
+ /// Bug
+ #[error("internal error")]
+ Bug(#[from] Bug),
+}
+
+/// Memory pressure means this data structure (or other facility) was torn down
+///
+/// Error type suitable for use by data structures and facilities
+/// which participate in memory tracking.
+///
+/// Convertible from a [`tor_memtrack::Error`](enum@Error),
+/// or constructible via `Default` or [`new`](MemoryReclaimedError::new).
+#[derive(Debug, Clone, Error, Default)]
+#[non_exhaustive]
+#[error("{0}")]
+pub struct MemoryReclaimedError(ReclaimedErrorInner);
+
+/// Content of a [`MemoryReclaimedError`]
+// Separate struct so we don't expose the variants
+#[derive(Debug, Clone, Error, Default)]
+enum ReclaimedErrorInner {
+ /// Collapsed, from `ReclaimedError::new`
+ #[error("data structure discarded due to memory pressure")]
+ #[default]
+ Collapsed,
+
+ /// Othere error from tracker
+ #[error("{0}")]
+ TrackerError(#[from] Error),
+}
+
+/// An error occurring when setting up a memory quota tracker
+#[derive(Debug, Clone, Error)]
+#[non_exhaustive]
+pub enum StartupError {
+ /// Task spawn failed
+ #[error("couldn't spawn reclamation task")]
+ Spawn(#[source] Arc<SpawnError>),
+}
+
+impl From<SpawnError> for StartupError {
+ fn from(e: SpawnError) -> StartupError {
+ StartupError::Spawn(Arc::new(e))
+ }
+}
+
+/// Tracker corrupted
+///
+/// Separate type so we don't expose `PoisonError -> crate::Error` conversion
+#[derive(Debug, Clone, Error)]
+#[error("poisoned (corrupted)")]
+pub(crate) struct TrackerCorrupted;
+
+impl<T> From<PoisonError<T>> for TrackerCorrupted {
+ fn from(_: PoisonError<T>) -> TrackerCorrupted {
+ TrackerCorrupted
+ }
+}
+
+impl From<TrackerCorrupted> for Error {
+ fn from(_: TrackerCorrupted) -> Error {
+ Error::TrackerCorrupted
+ }
+}
+
+/// Error returned when reclaim task crashes
+///
+/// Does not escape the crate; is used for logging.
+#[derive(Debug, Clone, Error)]
+pub(crate) enum ReclaimCrashed {
+ /// Previous bug, memory quota tracker is corrupted
+ #[error("memory tracker corrupted due to previous bug")]
+ TrackerCorrupted(#[from] TrackerCorrupted),
+
+ /// Bug
+ #[error("internal error")]
+ Bug(#[from] Bug),
+}
+
+impl MemoryReclaimedError {
+ /// Create a new `MemoryReclaimedError` (with no additional information)
+ pub fn new() -> Self {
+ MemoryReclaimedError::default()
+ }
+}
+
+impl From<Error> for MemoryReclaimedError {
+ fn from(e: Error) -> MemoryReclaimedError {
+ MemoryReclaimedError(e.into())
+ }
+}
+
+impl HasKind for MemoryReclaimedError {
+ fn kind(&self) -> ErrorKind {
+ use ErrorKind as EK;
+ use ReclaimedErrorInner as REI;
+ match &self.0 {
+ REI::Collapsed => EK::LocalResourceExhausted,
+ REI::TrackerError(e) => e.kind(),
+ }
+ }
+}
+
+impl HasKind for Error {
+ fn kind(&self) -> ErrorKind {
+ use Error as E;
+ use ErrorKind as EK;
+ match self {
+ E::TrackerShutdown => EK::ArtiShuttingDown,
+ E::AccountClosed => EK::LocalResourceExhausted,
+ E::ParticipantShutdown => EK::LocalResourceExhausted,
+ E::TrackerCorrupted => EK::Internal,
+ E::Bug(e) => e.kind(),
+ }
+ }
+}
diff --git a/crates/tor-memquota/src/internal_prelude.rs b/crates/tor-memquota/src/internal_prelude.rs
new file mode 100644
index 000000000..c727496f4
--- /dev/null
+++ b/crates/tor-memquota/src/internal_prelude.rs
@@ -0,0 +1,63 @@
+//! Internal prelude
+//!
+//! This file contains most of the imports we wish to use, throughout this crate.
+//!
+//! Every module does `use crate::internal_prelude::*;`
+//!
+//! Exceptions:
+//!
+//! * Names that are private to a module and its submodules
+//! are imported to the sub-modules via `use super::*`.
+//! (Thus, the sub-module inherits the prelude from its parent.)
+//!
+//! * Broad names from specific contexts, that are unsuitable for wide imports.
+//! For example, individual cell and message names from `tor-cell`,
+//! and the types from `tor_proto::stream` other than the high-level `DataStream`.
+
+pub(crate) use std::{
+ cmp::{Ordering, Reverse},
+ collections::{BinaryHeap, HashSet},
+ fmt::{self, Debug, Display},
+ future::Future,
+ marker::PhantomData,
+ mem,
+ ops::{Deref, DerefMut},
+ panic::{catch_unwind, AssertUnwindSafe},
+ pin::Pin,
+ sync::{Arc, Mutex, MutexGuard, PoisonError, Weak},
+};
+
+pub(crate) use futures::{
+ channel::mpsc,
+ task::{Spawn, SpawnError, SpawnExt as _},
+ FutureExt as _, StreamExt as _,
+};
+
+pub(crate) use {
+ derive_builder::Builder,
+ derive_deftly::{define_derive_deftly, Deftly},
+ derive_more::{Deref, DerefMut, From, Into},
+ educe::Educe,
+ serde::{Deserialize, Serialize},
+ slotmap::SlotMap,
+ static_assertions::assert_not_impl_any,
+ thiserror::Error,
+ tracing::{error, info},
+ void::{ResultVoidExt as _, Void},
+};
+
+pub(crate) use {
+ tor_config::ConfigBuildError,
+ tor_error::{error_report, internal, into_internal, Bug, ErrorKind, HasKind},
+ tor_log_ratelim::log_ratelim,
+ tor_rtcompat::CoarseInstant,
+};
+
+pub(crate) use crate::{
+ config::Config,
+ drop_bomb::{DropBomb, DropBombCondition},
+ drop_reentrancy,
+ error::{Error, ReclaimCrashed, StartupError, TrackerCorrupted},
+ refcount,
+ utils::{DefaultExtTake, Qty},
+};
diff --git a/crates/tor-memquota/src/lib.rs b/crates/tor-memquota/src/lib.rs
new file mode 100644
index 000000000..f6d5ad21b
--- /dev/null
+++ b/crates/tor-memquota/src/lib.rs
@@ -0,0 +1,206 @@
+#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
+#![doc = include_str!("../README.md")]
+
+//! ## Intended behavour
+//!
+//! In normal operation we try to track as little state as possible, cheaply.
+//! We do track total memory use in nominal bytes
+//! (but a little approximately).
+//!
+//! When we exceed the quota, we engage a more expensive algorithm:
+//! we build a heap to select oldest victims, and
+//! we use the heap to keep reducing memory
+//! until we go below a low-water mark (hysteresis).
+//!
+//! ## Key concepts
+//!
+//! * **Tracker**:
+//! Instance of the memory quota system
+//! Each tracker has a notion of how much memory its participants
+//! are allowed to use, in aggregate.
+//! Tracks memory usage by all the Accounts and Participants.
+//! Different Trackers are completely independent.
+//!
+//! * **Account**:
+//! all memory used within the same Account is treated equally,
+//! and reclamation also happens on an account-by-account basis.
+//! (Each Account is with one Tracker.)
+//!
+//! * **Participant**:
+//! one data structure that uses memory.
+//! Each Participant is linked to *one* Account. An account has *one or more* Participants.
+//! (An Account can exist with zero Participants, but can't then claim memory.)
+//! A Participant provides a `dyn IsParticipant` to the memory system;
+//! in turn, the memory system provides the Participant with a `Participation` -
+//! a handle for tracking memory alloc/free.
+//!
+//! * **Child Account**/**Parent Account**:
+//! An Account may have a Parent.
+//! When a tracker requests memory reclamation from a Parent,
+//! it will also request it of all that Parent's Children (but not vice versa).
+//!
+//! * **Data age**:
+//! Each Participant must be able to say what the oldest data is, that it is storing.
+//! The reclamation policy is to try to free the oldest data.
+//!
+//! * **Reclamation**:
+//! When a Tracker decides that too much memory is being used,
+//! it will select a victim Account based on the data age.
+//! It will then ask *every Participant* in that Account,
+//! and every Participant in every Child of that Account,
+//! to reclaim memory.
+//! A Participant responds by freeing at least some memory,
+//! according to the reclamation request, and tells the Tracker when it has done so.
+//!
+//! * **Reclamation strategy**:
+//! To avoid too-frequent reclamation, once reclamation has started,
+//! it will continue until a low-water mark is reached, significantly lower than the quota.
+//! I.e. the system has a hysteresis.
+// TODO we haven't implemented the queue wrapper yet
+// ! The only currently implemented higher-level Participant is
+// ! a queue which responds to a reclamation request
+// ! by completely destroying itself and freeing all its data.
+//!
+//! * **Approximate** (both in time and space):
+//! The memory quota system is not completely precise.
+//! Participants need not report their use precisely,
+//! but the errors should be reasonably small, and bounded.
+//! Likewise, the enforcement is not precise:
+//! reclamation may start slightly too early, or too late;
+//! but the memory use will be bounded below by O(number of participants)
+//! and above by O(1) (plus errors from the participants).
+//! Reclamation is not immediate, and is dependent on task scheduling;
+//! during memory pressure the quota may be exceeded;
+//! new allocations are not prevented while attempts at reclamation are ongoing.
+//!
+// TODO we haven't implemented the queue wrapper yet
+// ! * **Queues**:
+// ! We provide a higher-level API that wraps an mpsc queue and turns it into a Participant.
+// !
+//! ## Ownership and Arc keeping-alive
+//!
+//! * Somewhere, someone must keep an `Account` to keep the account open.
+//! Ie, the principal object corresponding to the accountholder should contain an `Account`.
+//!
+//! * `Arc<MemoryTracker>` holds `Weak<dyn IsParticipant>`.
+//! If the tracker finds the `IsParticipant` has vanished,
+//! it assumes this means that the Participant is being destroyed and
+//! it can treat all of the memory it claimed as freed.
+//!
+//! * Each participant holds a `Participation`.
+//! A `Participation` may be invalidated by collapse of the underlying Account,
+//! which may be triggered in any number of ways.
+//!
+//! * A `Participation` does *not* keep its `Account` alive.
+//! Ie, it has only a weak reference to the Account.
+//!
+//! * A Participant's implementor of `IsParticipant` may hold a `Participation`.
+//! If the `impl IsParticipant` is also the principal accountholder object,
+//! it must hold an `Account` too.
+//!
+//! * Child/parent accounts do not imply any keeping-alive relationship.
+//! It's just that a reclamation request to a parent (if it still exists)
+//! will also be made to its children.
+//!
+//!
+//! ```text
+//! accountholder =======================================>* Participant
+//! (impl IsParticipant)
+//! ||
+//! || ^ ||
+//! || | ||
+//! || global Weak<dyn>| ||
+//! || || | ||
+//! \/* \/ | ||
+//! | ||
+//! Account *===========> MemoryTracker ------------------' ||
+//! ||
+//! ^ ||
+//! | \/
+//! |
+//! `-------------------------------------------------* Participation
+//!
+//!
+//!
+//! accountholder which is also directly the Participant ==============\
+//! (impl IsParticipant) ||
+//! ^ ||
+//! || | ||
+//! || | ||
+//! || global |Weak<dyn> ||
+//! || || | ||
+//! \/ \/ | ||
+//! ||
+//! Account *===========> MemoryTracker ||
+//! ||
+//! ^ ||
+//! | \/
+//! |
+//! `-------------------------------------------------* Participation
+//!
+//! ```
+
+// @@ begin lint list maintained by maint/add_warning @@
+#![cfg_attr(not(ci_arti_stable), allow(renamed_and_removed_lints))]
+#![cfg_attr(not(ci_arti_nightly), allow(unknown_lints))]
+#![warn(missing_docs)]
+#![warn(noop_method_call)]
+#![warn(unreachable_pub)]
+#![warn(clippy::all)]
+#![deny(clippy::await_holding_lock)]
+#![deny(clippy::cargo_common_metadata)]
+#![deny(clippy::cast_lossless)]
+#![deny(clippy::checked_conversions)]
+#![warn(clippy::cognitive_complexity)]
+#![deny(clippy::debug_assert_with_mut_call)]
+#![deny(clippy::exhaustive_enums)]
+#![deny(clippy::exhaustive_structs)]
+#![deny(clippy::expl_impl_clone_on_copy)]
+#![deny(clippy::fallible_impl_from)]
+#![deny(clippy::implicit_clone)]
+#![deny(clippy::large_stack_arrays)]
+#![warn(clippy::manual_ok_or)]
+#![deny(clippy::missing_docs_in_private_items)]
+#![warn(clippy::needless_borrow)]
+#![warn(clippy::needless_pass_by_value)]
+#![warn(clippy::option_option)]
+#![deny(clippy::print_stderr)]
+#![deny(clippy::print_stdout)]
+#![warn(clippy::rc_buffer)]
+#![deny(clippy::ref_option_ref)]
+#![warn(clippy::semicolon_if_nothing_returned)]
+#![warn(clippy::trait_duplication_in_bounds)]
+#![deny(clippy::unchecked_duration_subtraction)]
+#![deny(clippy::unnecessary_wraps)]
+#![warn(clippy::unseparated_literal_suffix)]
+#![deny(clippy::unwrap_used)]
+#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
+#![allow(clippy::uninlined_format_args)]
+#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
+#![allow(clippy::result_large_err)] // temporary workaround for arti#587
+#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
+//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
+#![allow(clippy::blocks_in_conditions)] // TODO #1176
+
+// Internal supporting modules
+#[macro_use]
+mod drop_bomb;
+mod drop_reentrancy;
+mod internal_prelude;
+#[macro_use]
+mod refcount;
+mod utils;
+
+// Modules with public items
+mod config;
+mod error;
+pub mod mtracker;
+
+//---------- re-exports at the crate root ----------
+
+pub use config::{Config, ConfigBuilder};
+pub use error::{Error, MemoryReclaimedError, StartupError};
+pub use mtracker::MemoryQuotaTracker;
+
+/// `Result` whose `Err` is [`tor_memtrack::Error`](Error)
+pub type Result<T> = std::result::Result<T, Error>;
diff --git a/crates/tor-memquota/src/mtracker.rs b/crates/tor-memquota/src/mtracker.rs
new file mode 100644
index 000000000..33333a930
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker.rs
@@ -0,0 +1,1047 @@
+//! Memory quota tracker, core and low-level API
+//!
+//! # Example
+//!
+//! ```
+//! use std::{collections::VecDeque, sync::{Arc, Mutex}};
+//! use tor_rtcompat::{CoarseInstant, CoarseTimeProvider, PreferredRuntime};
+//! use tor_memquota::{mtracker, MemoryQuotaTracker, MemoryReclaimedError};
+//! use void::{ResultVoidExt, Void};
+//!
+//! #[derive(Debug)]
+//! struct TrackingQueue(Mutex<Result<Inner, MemoryReclaimedError>>);
+//! #[derive(Debug)]
+//! struct Inner {
+//! partn: mtracker::Participation,
+//! data: VecDeque<(Box<[u8]>, CoarseInstant)>,
+//! }
+//!
+//! impl TrackingQueue {
+//! fn push(&self, now: CoarseInstant, bytes: Box<[u8]>) -> Result<(), MemoryReclaimedError> {
+//! let mut inner = self.0.lock().unwrap();
+//! let inner = inner.as_mut().map_err(|e| e.clone())?;
+//! inner.partn.claim(bytes.len())?;
+//! inner.data.push_back((bytes, now));
+//! Ok(())
+//! }
+//! }
+//!
+//! impl mtracker::IsParticipant for TrackingQueue {
+//! fn get_oldest(&self) -> Option<CoarseInstant> {
+//! let inner = self.0.lock().unwrap();
+//! Some(inner.as_ref().ok()?.data.front()?.1)
+//! }
+//! fn reclaim(self: Arc<Self>) -> mtracker::ReclaimFuture {
+//! let mut inner = self.0.lock().unwrap();
+//! *inner = Err(MemoryReclaimedError::new());
+//! Box::pin(async { mtracker::Reclaimed::Collapsing })
+//! }
+//! }
+//!
+//! let runtime = PreferredRuntime::create().unwrap();
+//! let config = tor_memquota::Config::builder().max(1024*1024*1024).build().unwrap();
+//! let trk = MemoryQuotaTracker::new(&runtime, config).unwrap();
+//! let account = trk.new_account(None).unwrap();
+//!
+//! let queue: Arc<TrackingQueue> = account.register_participant_with(
+//! runtime.now_coarse(),
+//! |partn| {
+//! Ok::<_, Void>(Arc::new(TrackingQueue(Mutex::new(Ok(Inner {
+//! partn,
+//! data: VecDeque::new(),
+//! })))))
+//! },
+//! ).unwrap().void_unwrap();
+//!
+//! queue.push(runtime.now_coarse(), Box::new([0; 24])).unwrap();
+//! ```
+//
+// For key internal documentation about the data structure, see the doc comment for
+// `struct State` (down in the middle of the file).
+
+use crate::internal_prelude::*;
+
+mod bookkeeping;
+mod reclaim;
+mod total_qty_notifier;
+
+#[cfg(test)]
+mod test;
+
+use bookkeeping::{BookkeepableQty, ClaimedQty, ParticipQty, TotalQty};
+use total_qty_notifier::TotalQtyNotifier;
+
+/// Maximum amount we'll "cache" locally in a [`Participation`]
+///
+/// ie maximum value of `Participation.cache`.
+//
+// TODO is this a good amount? should it be configurable?
+pub(crate) const MAX_CACHE: Qty = Qty(16384);
+
+/// Target cache size when we seem to be claiming
+const TARGET_CACHE_CLAIMING: Qty = Qty(MAX_CACHE.as_usize() * 3 / 4);
+/// Target cache size when we seem to be releasing
+#[allow(clippy::identity_op)] // consistency
+const TARGET_CACHE_RELEASING: Qty = Qty(MAX_CACHE.as_usize() * 1 / 4);
+
+//---------- public data types ----------
+
+/// Memory data tracker
+///
+/// Instance of the memory quota system.
+///
+/// Usually found as `Arc<MemoryQuotaTracker>`.
+#[derive(Debug)]
+pub struct MemoryQuotaTracker {
+ /// The actual tracker state etc.
+ state: Mutex<State>,
+}
+
+/// Handle onto an Account
+///
+/// An `Account` is a handle. All clones refer to the same underlying conceptual Account.
+///
+/// `Account`s are created using [`MemoryQuotaTracker::new_account`].
+#[derive(Educe)]
+#[educe(Debug)]
+pub struct Account {
+ /// The account ID
+ aid: refcount::Ref<AId>,
+
+ /// The underlying tracker
+ #[educe(Debug(ignore))]
+ tracker: Arc<MemoryQuotaTracker>,
+}
+
+/// Weak handle onto an Account
+///
+/// Like [`Account`], but doesn't keep the account alive.
+/// Must be upgraded before use.
+//
+// Doesn't count for ARecord.account_clones
+//
+// We can't lift out Arc, so that the caller sees `Arc<Account>`,
+// because an Account is Arc<MemoryQuotaTracker> plus AId,
+// not Arc of something account-specific.
+#[derive(Clone, Educe)]
+#[educe(Debug)]
+pub struct WeakAccount {
+ /// The account ID
+ aid: AId,
+
+ /// The underlying tracker
+ #[educe(Debug(ignore))]
+ tracker: Weak<MemoryQuotaTracker>,
+}
+
+/// Handle onto a participant's participation in a tracker
+///
+/// `Participation` is a handle. All clones are for use by the same conceptual Participant.
+/// It doesn't keep the underlying Account alive.
+///
+/// `Participation`s are created by registering new participants,
+/// for example using [`Account::register_participant`].
+///
+/// Variables of this type are often named `partn`.
+#[derive(Debug)]
+pub struct Participation {
+ /// Participant id
+ pid: refcount::Ref<PId>,
+
+ /// Account id
+ aid: AId,
+
+ /// The underlying tracker
+ tracker: Weak<MemoryQuotaTracker>,
+
+ /// Quota we have preemptively claimed for use by this Account
+ ///
+ /// Has been added to `PRecord.used`,
+ /// but not yet returned by `Participation::claim`.
+ ///
+ /// This cache field arranges that most of the time we don't have to hammer a
+ /// single cache line.
+ ///
+ /// The value here is bounded by a configured limit.
+ ///
+ /// Invariants on memory accounting:
+ ///
+ /// * `Participation.cache < configured limit`
+ /// * `PRecord.used = Participation.cache + Σ Participation::claim - Σ P'n::release`
+ /// except if `PRecord` has been deleted
+ /// (ie when we aren't tracking any more and think the Participant is `Collapsing`).
+ /// * `Σ PRecord.used = State.total_used`
+ ///
+ /// Enforcement of these invariants is partially assured by
+ /// types in [`bookkeeping`].
+ cache: ClaimedQty,
+}
+
+/// Participants provide an impl of the hooks in this trait
+///
+/// Trait implemented by client of the memtrack API.
+///
+/// # Panic handling, "unwind safety"
+///
+/// If these methods panic, the memory tracker will tear down its records of the
+/// participant, preventing future allocations.
+///
+/// But, it's not guaranteed that these methods on `IsParticipant` won't be called again,
+/// even if they have already panicked on a previous occasion.
+/// Thus the implementations might see "broken invariants"
+/// as discussed in the docs for `std::panic::UnwindSafe`.
+///
+/// Nevertheless we don't make `RefUnwindSafe` a supertrait of `IsParticipant`.
+/// That would force the caller to mark *all* their methods unwind-safe,
+/// which is unreasonable (and probably undesirable).
+///
+/// Variables which are `IsParticipant` are often named `particip`.
+pub trait IsParticipant: Debug + Send + Sync + 'static {
+ /// Return the age of the oldest data held by this Participant
+ ///
+ /// `None` means this Participant holds no data.
+ ///
+ /// # Performance and reentrancy
+ ///
+ /// This function runs with the `MemoryQuotaTracker`'s internal global lock held.
+ /// Therefore:
+ ///
+ /// * It must be fast.
+ /// * it *must not* call back into methods from [`tracker`](crate::mtracker).
+ /// * It *must not* even `Clone` or `Drop` a [`MemoryQuotaTracker`],
+ /// [`Account`], or [`Participation`].
+ fn get_oldest(&self) -> Option<CoarseInstant>;
+
+ /// Start memory reclamation
+ ///
+ /// The Participant should start to free all of its memory,
+ /// and then return `Reclaimed::Collapsing`.
+ //
+ // In the future:
+ //
+ // Should free *at least* all memory at least as old as discard_...
+ //
+ // v1 of the actual implementation might not have `discard_everything_as_old_as`
+ // and `but_can_stop_discarding_...`,
+ // and might therefore only support Reclaimed::Collapsing
+ fn reclaim(
+ self: Arc<Self>,
+ // Future:
+ // discard_everything_as_old_as_this: RoughTime,
+ // but_can_stop_discarding_after_freeing_this_much: Qty,
+ ) -> ReclaimFuture;
+}
+
+/// Future returned by the [`IsParticipant::reclaim`] reclamation request
+pub type ReclaimFuture = Pin<Box<dyn Future<Output = Reclaimed> + Send + Sync>>;
+
+/// Outcome of [`IsParticipant::reclaim`]
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
+#[non_exhaustive]
+pub enum Reclaimed {
+ /// Participant is responding to reclamation by collapsing completely.
+ ///
+ /// All memory will be freed and `release`'d soon (if it hasn't been already).
+ /// `MemoryQuotaTracker` should forget the Participant and all memory it used, right away.
+ ///
+ /// Currently this is the only supported behaviour.
+ Collapsing,
+ // Future:
+ // /// Participant has now reclaimed some memory as instructed
+ // ///
+ // /// If this is not sufficient, tracker must call reclaim() again.
+ // /// (We may not want to implement Partial right away but the API
+ // /// ought to support it so let's think about it now, even if we don't implement it.)
+ // Partial,
+}
+
+//---------- principal data structure ----------
+
+slotmap::new_key_type! {
+ /// Identifies an Account
+ ///
+ /// After an account is torn down, the `AId` becomes invalid
+ /// and attempts to use it will give an error.
+ ///
+ /// The same `AId` won't be reused for a later Account.
+ struct AId;
+
+ /// Identifies a Participant within an Account
+ ///
+ /// Ie, PId is scoped within in the context of an account.
+ ///
+ /// As with `AId`, a `PId` is invalid after the
+ /// participation is torn down, and is not reused.
+ struct PId;
+}
+
+/// Memory tracker inner, including mutable state
+///
+/// # Module internal documentation
+///
+/// ## Data structure
+///
+/// * [`MemoryQuotaTracker`] contains mutex-protected `State`.
+/// * The `State` contains a [`SlotMap`] of account records [`ARecord`].
+/// * Each `ARecord` contains a `SlotMap` of participant records [`PRecord`].
+///
+/// The handles [`Account`], [`WeakAccount`], and [`Participation`],
+/// each contain a reference (`Arc`/`Weak`) to the `MemoryQuotaTracker`,
+/// and the necessary slotmap keys.
+///
+/// The `ARecord` and `PRecord` each contain a reference count,
+/// which is used to clean up when all the handles are gone.
+///
+/// The slotmap keys which count for the reference count (ie, strong references)
+/// are stored as [`refcount::Ref`],
+/// which helps assure correct reference counting.
+/// (Bare ids [`AId`] and [`PId`] are weak references.)
+///
+/// ## Data structure lookup
+///
+/// Given a reference to the tracker, and some ids, the macro `find_in_tracker!`
+/// is used to obtain mutable references to the `ARecord` and (if applicable) `PRecord`.
+///
+/// ## Bookkeeping
+///
+/// We use separate types for quantities of memory in various "states",
+/// rather than working with raw quantities.
+///
+/// The types, and the legitimate transactions, are in `bookkeeping`.
+///
+/// ## Reentrancy (esp. `Drop` and `Clone`)
+///
+/// When the handle structs are dropped or cloned, they must manipulate the refcount(s).
+/// So they must take the lock.
+/// Therefore, an `Account` and `Participation` may not be dropped with the lock held!
+///
+/// Internally, this is actually fairly straightforward:
+/// we take handles by reference, and constructors only make them at the last moment on return,
+/// so our internal code here, in this module, doesn't have owned handles.
+///
+/// We also need to worry about reentrantly reentering the tracker code, from user code.
+/// The user supplies a `dyn IsParticipant`.
+/// The principal methods are from [`IsParticipant`],
+/// for which we handle reentrancy in the docs.
+/// But we also implicitly invoke its `Drop` impl, which might in turn drop stuff of ours,
+/// such as [`Account`]s and [`Participation`]s, whose `Drop` impls need to take our lock.
+/// To make sure this isn't done reentrantly, we have a special newtype around it,
+/// and defer some of our drops during reclaim.
+/// That's in `drop_reentrancy` and `tracker::reclaim::deferred_drop`.
+///
+/// The `Debug` impl isn't of concern, since we don't call it ourselves.
+/// And we don't rely on it being `Clone`, since it's in an `Arc`.
+///
+/// ## Drop bombs
+///
+/// With `#[cfg(test)]`, several of our types have "drop bombs":
+/// they cause a panic if dropped inappropriately.
+/// This is intended to detect bad code paths during testing.
+#[derive(Debug, Deref, DerefMut)]
+struct State {
+ /// Global parts of state
+ ///
+ /// Broken out to allow passing both
+ /// `&mut Global` and `&mut ARecord`/`&mut PRecord`
+ /// to some function(s).
+ #[deref]
+ #[deref_mut]
+ global: Global,
+
+ /// Accounts
+ accounts: SlotMap<AId, ARecord>,
+}
+
+/// Global parts of `State`
+#[derive(Debug)]
+struct Global {
+ /// Total memory used
+ ///
+ /// Wrapper type for ensuring we wake up the reclaimation task
+ total_used: TotalQtyNotifier,
+
+ /// Configuration
+ config: Config,
+}
+
+/// Account record, within `State.accounts`
+#[derive(Debug)]
+#[must_use = "don't just drop, call auto_release"]
+struct ARecord {
+ /// Number of clones of `Account`; to know when to tear down the account
+ refcount: refcount::Count<AId>,
+
+ /// Child accounts
+ children: Vec<AId>,
+
+ /// Participants linked to this Account
+ ps: SlotMap<PId, PRecord>,
+}
+
+/// Participant record, within `ARecord.ps`
+#[derive(Debug)]
+#[must_use = "don't just drop, call auto_release"]
+struct PRecord {
+ /// Number of clones of `Participation`; to know when to tear down the participant
+ refcount: refcount::Count<PId>,
+
+ /// Memory usage of this participant
+ ///
+ /// Not 100% accurate, can lag, and be (boundedly) an overestimate
+ used: ParticipQty,
+
+ /// The hooks provided by the Participant
+ particip: drop_reentrancy::ProtectedWeak<dyn IsParticipant>,
+}
+
+//#################### IMPLEMENTATION ####################
+
+/// Given a `&Weak<MemoryQuotaTracker>`, find an account and maybe participant
+///
+/// ### Usage templates
+///
+/// ```rust,ignore
+/// find_in_tracker! {
+/// weak_tracker => + tracker, state;
+/// aid => arecord;
+/// [ pid => precord; ]
+/// [ ?Error | ?None ]
+/// };
+///
+/// find_in_tracker! {
+/// strong_tracker => state;
+/// .. // as above
+/// };
+/// ```
+///
+/// ### Input expressions (value arguments to the macro0
+///
+/// * `weak_tracker: &Weak<MemoryQuotaTracker>` (or equivalent)
+/// * `strong_tracker: &MemoryQuotaTracker` (or equivalent)
+/// * `aid: AId`
+/// * `pid: PId`
+///
+/// ### Generated bindings (identifier arguments to the macro)
+///
+/// * `tracker: Arc<MemoryQuotaTracker>`
+/// * `state: &mut State` (borrowed from a `MutexGuard<State>` borrowed from `tracker`)
+/// * `arecord: &mut ARecord` (mut borrowed from `state.accounts`)
+/// * `precord: &mut PRecord` (mut borrowed from `arecord.ps`)
+///
+/// There is no access to the `MutexGuard` itself.
+/// For control of the mutex release point, place `find_in_tracker!` in an enclosing block.
+///
+/// ### Error handling
+///
+/// If the tracker, account, or participant, can't be found,
+/// the macro returns early from the enclosing scope (using `?`).
+///
+/// If `Error` is specified, applies `?` to `Err(Error::...)`.
+/// If `None` is specified, just returns `None` (by applying `?` to None`).
+//
+// This has to be a macro because it makes a self-referential set of bindings.
+// Input syntax is a bit janky because macro_rules is so bad.
+// For an internal macro with ~9 call sites it's not worth making a big parsing contraption.
+macro_rules! find_in_tracker { {
+ // This `+` is needed because otherwise it's LL1-ambiguous and macro_rules can't cope
+ $tracker_input:expr => $( + $tracker:ident, )? $state:ident;
+ $aid:expr => $arecord:ident;
+ $( $pid:expr => $precord:ident; )?
+ // Either `Error` or None, to be passed to `find_in_tracker_eh!($eh ...: ...)`
+ // (We need this to be an un-repeated un-optional binding, because
+ // it is used within some other $( ... )?, and macro_rules gets confused.)
+ ? $eh:tt
+} => {
+ let tracker = &$tracker_input;
+ $(
+ let $tracker: Arc<MemoryQuotaTracker> = find_in_tracker_eh!(
+ $eh TrackerShutdown:
+ tracker.upgrade()
+ );
+ let tracker = &$tracker;
+ )?
+ let mut state: MutexGuard<State> = find_in_tracker_eh!(
+ $eh TrackerCorrupted:
+ tracker.state.lock().ok()
+ );
+ let $state: &mut State = &mut *state;
+ let aid: AId = $aid;
+ let $arecord: &mut ARecord = find_in_tracker_eh!(
+ $eh AccountClosed:
+ $state.accounts.get_mut(aid)
+ );
+ $(
+ let pid: PId = $pid;
+ let $precord: &mut PRecord = find_in_tracker_eh!(
+ $eh ParticipantShutdown:
+ $arecord.ps.get_mut(pid)
+ );
+ )?
+} }
+/// Error handling helper for `find_in_tracker`
+macro_rules! find_in_tracker_eh {
+ { None $variant:ident: $result:expr } => { $result? };
+ { Error $variant:ident: $result:expr } => { $result.ok_or(Error::$variant)? };
+}
+
+//========== impls on public types, including public methods and trait impls ==========
+
+//---------- MemoryQuotaTracker ----------
+
+impl MemoryQuotaTracker {
+ /// Set up a new `MemoryDataTracker`
+ pub fn new<R: Spawn>(runtime: &R, config: Config) -> Result<Arc<Self>, StartupError> {
+ let (reclaim_tx, reclaim_rx) = mpsc::channel(0 /* plus num_senders, ie 1 */);
+ let total_used = TotalQtyNotifier::new_zero(reclaim_tx);
+
+ let global = Global { total_used, config };
+ let accounts = SlotMap::default();
+ let state = Mutex::new(State { global, accounts });
+ let tracker = Arc::new(MemoryQuotaTracker { state });
+
+ // We don't provide a separate `launch_background_tasks`, because this task doesn't
+ // wake up periodically, or, indeed, do anything until the tracker is used.
+
+ let for_task = Arc::downgrade(&tracker);
+ runtime.spawn(reclaim::task(for_task, reclaim_rx))?;
+
+ Ok(tracker)
+ }
+
+ /// Make a new `Account`
+ ///
+ /// To actually record memory usage, a Participant must be added.
+ //
+ // Right now, parent can't be changed after construction of an Account,
+ // so circular accounts are impossible.
+ // But, we might choose to support that in the future.
+ // Circular parent relationships might need just a little care
+ // in the reclamation loop (to avoid infinitely looping),
+ // but aren't inherently unsupportable.
+ #[allow(clippy::redundant_closure_call)] // We have IEFEs for good reaons
+ pub fn new_account(self: &Arc<Self>, parent: Option<&Account>) -> crate::Result<Account> {
+ let mut state = self.lock()?;
+
+ let parent_aid_good = parent
+ .map(|parent| {
+ // Find and check the requested parent's Accountid
+
+ let parent_aid = *parent.aid;
+ let parent_arecord = state
+ .accounts
+ .get_mut(parent_aid)
+ .ok_or(Error::AccountClosed)?;
+
+ // Can we insert the new child without reallocating?
+ if !parent_arecord.children.spare_capacity_mut().is_empty() {
+ return Ok(parent_aid);
+ }
+
+ // No. Well, let's do some garbage collection.
+ // (Otherwise .children might grow without bound as accounts come and go)
+ //
+ // We would like to scan the accounts array while mutating this account.
+ // Instead, steal the children array temporarily and put the filtered one back.
+ // Must be infallible!
+ //
+ // The next line can't be in the closure (confuses borrowck)
+ let mut parent_children = mem::take(&mut parent_arecord.children);
+ (|| {
+ parent_children.retain(|child_aid| state.accounts.contains_key(*child_aid));
+
+ // Put the filtered list back, so sanity is restored.
+ state
+ .accounts
+ .get_mut(parent_aid)
+ .expect("parent vanished!")
+ .children = parent_children;
+ })();
+
+ Ok::<_, Error>(parent_aid)
+ })
+ .transpose()?;
+
+ // We have resolved the parent AId and prepared to add the new account to its list of
+ // children. We still hold the lock, so nothing can have changed.
+
+ // commitment - infallible IEFE assures that so we don't do half of it
+ Ok((|| {
+ let aid = refcount::slotmap_insert(&mut state.accounts, |refcount| ARecord {
+ refcount,
+ children: vec![],
+ ps: SlotMap::default(),
+ });
+
+ if let Some(parent_aid_good) = parent_aid_good {
+ state
+ .accounts
+ .get_mut(parent_aid_good)
+ .expect("parent vanished!")
+ .children
+ .push(*aid);
+ }
+
+ let tracker = self.clone();
+ Account { aid, tracker } // don't make this fallible, see above.
+ })())
+ }
+
+ /// Obtain the lock on the state
+ fn lock(&self) -> Result<MutexGuard<State>, TrackerCorrupted> {
+ Ok(self.state.lock()?)
+ }
+}
+
+//---------- Account ----------
+
+impl Account {
+ /// Register a new Participant
+ ///
+ /// Returns the [`Participation`], which can be used to record memory allocations.
+ ///
+ /// Often, your implementation of [`IsParticipant`] wants to contain the [`Participation`].
+ /// If so, use [`register_participant_with`](Account::register_participant_with) instead.
+ pub fn register_participant(
+ &self,
+ particip: Weak<dyn IsParticipant>,
+ ) -> Result<Participation, Error> {
+ let aid = *self.aid;
+ find_in_tracker! {
+ self.tracker => state;
+ aid => arecord;
+ ?Error
+ }
+
+ let (pid, cache) = refcount::slotmap_try_insert(&mut arecord.ps, |refcount| {
+ let mut precord = PRecord {
+ refcount,
+ used: ParticipQty::ZERO,
+ particip: drop_reentrancy::ProtectedWeak::new(particip),
+ };
+ let cache =
+ state
+ .global
+ .total_used
+ .claim(&mut precord, MAX_CACHE, &state.global.config)?;
+ Ok::<_, Error>((precord, cache))
+ })?;
+
+ let tracker = Arc::downgrade(&self.tracker);
+ Ok(Participation {
+ tracker,
+ pid,
+ aid,
+ cache,
+ })
+ }
+
+ /// Set the callbacks for a Participant (identified by its weak ids)
+ fn set_participant_callbacks(
+ &self,
+ aid: AId,
+ pid: PId,
+ particip: drop_reentrancy::ProtectedWeak<dyn IsParticipant>,
+ ) -> Result<(), Error> {
+ find_in_tracker! {
+ self.tracker => state;
+ aid => arecord;
+ pid => precord;
+ ?Error
+ }
+ precord.particip = particip;
+ Ok(())
+ }
+
+ /// Register a new Participant using a constructor
+ ///
+ /// Passes `constructor` a [`Participation`] for the nascent Participant.
+ /// Returns the `P: IsParticipant` provided by the constructor.
+ ///
+ /// For use when your `impl `[`IsParticipant`] wants to own the `Participation`.
+ ///
+ /// # Re-entrancy guarantees
+ ///
+ /// The `Participation` *may* be used by `constructor` for claiming memory use,
+ /// even during construction.
+ /// `constructor` may also clone the `Participation`, etc.
+ ///
+ /// Reclamation callbacks (via the `P as IsParticipant` impl) cannot occur
+ /// until `constructor` returns.
+ ///
+ /// # Error handling
+ ///
+ /// Failures can occur before `constructor` is called,
+ /// or be detected afterwards.
+ /// If a failure is detected after `constructor` returns,
+ /// the `Arc<P>` from `constructor` will be dropped
+ /// (resulting in `P` being dropped, unless `constructor` kept another clone of it).
+ ///
+ /// `constructor` may also fail (throwing a different error type, `E`),
+ /// in which case `register_participant_with` returns `Ok(Err(E))`.
+ ///
+ /// On successful setup of the Participant, returns `Ok(Ok(Arc<P>))`.
+ pub fn register_participant_with<P: IsParticipant, E>(
+ &self,
+ now: CoarseInstant,
+ constructor: impl FnOnce(Participation) -> Result<Arc<P>, E>,
+ ) -> Result<Result<Arc<P>, E>, Error> {
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ /// Temporary participant, which stands in during constructon
+ #[derive(Debug)]
+ struct TemporaryParticipant {
+ /// The age, which is right now. We hope this is all fast!
+ now: CoarseInstant,
+ /// Did someone call reclaim() ?
+ collapsing: AtomicBool,
+ }
+
+ impl IsParticipant for TemporaryParticipant {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ Some(self.now)
+ }
+ fn reclaim(self: Arc<Self>) -> ReclaimFuture {
+ self.collapsing.store(true, Ordering::Release);
+ Box::pin(async { Reclaimed::Collapsing })
+ }
+ }
+
+ let temp_particip = Arc::new(TemporaryParticipant {
+ now,
+ collapsing: false.into(),
+ });
+
+ let partn = self.register_participant(Arc::downgrade(&temp_particip) as _)?;
+ let aid = partn.aid;
+ let pid_weak = *partn.pid;
+
+ // We don't hold the state lock here. register_participant took it and released it.
+ // This is important, because the constructor might call claim!
+ // (And, also, we don't want the constructor panicking to poison the whole tracker.)
+ // But it means there can be quite a lot of concurrent excitement,
+ // including, theoretically, a possible reclaim.
+ let particip = match constructor(partn) {
+ Ok(y) => y,
+ Err(e) => return Ok(Err(e)),
+ };
+ let particip = drop_reentrancy::ProtectedArc::new(particip);
+
+ // IEFE prevents use from accidentally dropping `particip` until we mean to
+ let r = (|| {
+ let weak = {
+ let weak = particip.downgrade();
+
+ // Trait cast, from Weak<P> to Weak<dyn IsParticipant>.
+ // We can only do this for a primitive, so we must unprotect
+ // the Weak, converr it, and protect it again.
+ drop_reentrancy::ProtectedWeak::new(weak.unprotect() as _)
+ };
+ self.set_participant_callbacks(aid, pid_weak, weak)?;
+
+ if temp_particip.collapsing.load(Ordering::Acquire) {
+ return Err(Error::ParticipantShutdown);
+ }
+ Ok(())
+ })();
+
+ let particip = particip.promise_dropping_is_ok();
+ r?;
+ Ok(Ok(particip))
+ }
+
+ /// Obtains a handle for the `MemoryQuotaTracker`
+ pub fn tracker(&self) -> Arc<MemoryQuotaTracker> {
+ self.tracker.clone()
+ }
+
+ /// Downgrade to a weak handle for the same Account
+ pub fn downgrade(&self) -> WeakAccount {
+ WeakAccount {
+ aid: *self.aid,
+ tracker: Arc::downgrade(&self.tracker),
+ }
+ }
+}
+
+impl Clone for Account {
+ fn clone(&self) -> Account {
+ let tracker = self.tracker.clone();
+ let aid = (|| {
+ let aid = *self.aid;
+ find_in_tracker! {
+ tracker => state;
+ aid => arecord;
+ ?None
+ }
+ let aid = refcount::Ref::new(aid, &mut arecord.refcount).ok()?;
+ // commitment point
+ Some(aid)
+ })()
+ .unwrap_or_else(|| {
+ // Either the account has been closed, or our refcount overflowed.
+ // Return a busted `Account`, which always fails when we try to use it.
+ //
+ // If the problem was a refcount overflow, we're technically violating the
+ // documented behaviour, since the returned `Account` isn't equivalent
+ // to the original. We could instead choose to tear down the Account;
+ // that would be legal; but it's a lot of code to marginally change the
+ // behaviour for a very unlikely situation.
+ refcount::Ref::null()
+ });
+ Account { aid, tracker }
+ }
+}
+
+impl Drop for Account {
+ fn drop(&mut self) {
+ (|| {
+ find_in_tracker! {
+ self.tracker => state;
+ *self.aid => arecord;
+ ?None
+ }
+ if let Some(refcount::Garbage(mut removed)) =
+ slotmap_dec_ref!(&mut state.accounts, self.aid.take(), &mut arecord.refcount)
+ {
+ // This account is gone. Automatically release everything.
+ removed.auto_release(state);
+ }
+ Some(())
+ })()
+ .unwrap_or_else(|| {
+ // Account has been torn down. Dispose of the strong ref.
+ // (This has no effect except in cfg(test), when it defuses the drop bombs)
+ self.aid.take().dispose_container_destroyed();
+ });
+ }
+}
+
+//---------- WeakAccount ----------
+
+impl WeakAccount {
+ /// Upgrade to an `Account`, if the account still exists
+ pub fn upgrade(&self) -> crate::Result<Account> {
+ let aid = self.aid;
+ // (we must use a block, and can't use find_in_tracker's upgrade, because borrowck)
+ let tracker = self.tracker.upgrade().ok_or(Error::TrackerShutdown)?;
+ let aid = {
+ find_in_tracker! {
+ tracker => state;
+ aid => arecord;
+ ?Error
+ }
+ refcount::Ref::new(aid, &mut arecord.refcount)?
+ // commitment point
+ };
+ Ok(Account { aid, tracker })
+ }
+
+ /// Obtains a handle onto the `MemoryQuotaTracker`
+ ///
+ /// The returned handle is itself weak, and needs to be upgraded before use.
+ pub fn tracker(&self) -> Weak<MemoryQuotaTracker> {
+ self.tracker.clone()
+ }
+}
+
+//---------- Participation ----------
+
+impl Participation {
+ /// Record that some memory has been (or will be) allocated
+ pub fn claim(&mut self, want: usize) -> crate::Result<()> {
+ self.claim_qty(Qty(want))
+ }
+
+ /// Record that some memory has been (or will be) allocated (using `Qty`)
+ pub(crate) fn claim_qty(&mut self, want: Qty) -> crate::Result<()> {
+ if let Some(got) = self.cache.split_off(want) {
+ return got.claim_return_to_participant();
+ }
+
+ find_in_tracker! {
+ self.tracker => + tracker, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?Error
+ };
+
+ let mut claim = |want| -> Result<ClaimedQty, _> {
+ state
+ .global
+ .total_used
+ .claim(precord, want, &state.global.config)
+ };
+ let got = claim(want)?;
+
+ if want <= TARGET_CACHE_CLAIMING {
+ // While we're here, fill the cache to TARGET_CACHE_CLAIMING.
+ // Cannot underflow: cache < want (since we failed at `got` earlier
+ // and we've just checked want <= TARGET_CACHE_CLAIMING.
+ let want_more_cache = Qty(*TARGET_CACHE_CLAIMING - *self.cache.as_raw());
+ if let Ok(add_cache) = claim(want_more_cache) {
+ // On error, just don't do this; presumably the error will show up later
+ // (we mustn't early exit here, because we've got the claim in our hand).
+ self.cache.merge_into(add_cache);
+ }
+ }
+ got.claim_return_to_participant()
+ }
+
+ /// Record that some memory has been (or will be) freed by a participant
+ pub fn release(&mut self, have: usize) // infallible
+ {
+ self.release_qty(Qty(have));
+ }
+
+ /// Record that some memory has been (or will be) freed by a participant (using `Qty`)
+ pub(crate) fn release_qty(&mut self, have: Qty) // infallible
+ {
+ let have = ClaimedQty::release_got_from_participant(have);
+ self.cache.merge_into(have);
+ if self.cache > MAX_CACHE {
+ match (|| {
+ find_in_tracker! {
+ self.tracker => + tracker, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?None
+ }
+ let return_from_cache = Qty(*self.cache.as_raw() - *TARGET_CACHE_RELEASING);
+ let from_cache = self.cache.split_off(return_from_cache).expect("impossible");
+ state.global.total_used.release(precord, from_cache);
+ Some(())
+ })() {
+ Some(()) => {} // we've given our cache back to the tracker
+ None => {
+ // account (or whole tracker!) is gone
+ // throw away the cache so that we don't take this path again for a bit
+ self.cache.take().dispose_participant_destroyed();
+ }
+ }
+ }
+ }
+
+ /// Obtain a handle onto the account
+ ///
+ /// The returned handle is weak, and needs to be upgraded before use,
+ /// since a [`Participation`] doesn't keep its Account alive.
+ ///
+ /// The returned `WeakAccount` is equivalent to
+ /// all the other account handles for the same account.
+ pub fn account(&self) -> WeakAccount {
+ WeakAccount {
+ aid: self.aid,
+ tracker: self.tracker.clone(),
+ }
+ }
+}
+
+impl Clone for Participation {
+ fn clone(&self) -> Participation {
+ let aid = self.aid;
+ let cache = ClaimedQty::ZERO;
+ let tracker: Weak<_> = self.tracker.clone();
+ let pid = (|| {
+ let pid = *self.pid;
+ find_in_tracker! {
+ self.tracker => + tracker_strong, state;
+ aid => _arecord;
+ pid => precord;
+ ?None
+ }
+ let pid = refcount::Ref::new(pid, &mut precord.refcount).ok()?;
+ // commitment point
+ Some(pid)
+ })()
+ .unwrap_or_else(|| {
+ // The account has been closed, the participant torn down, or the refcount
+ // overflowed. We can a busted `Participation`.
+ //
+ // We *haven't* incremented the refcount, so we mustn't return pid as a strong
+ // reference. We aren't supposed to count towards PRecord.refcount, we we *can*
+ // return the weak reference aid. (`refcount` type-fu assures this is correct.)
+ //
+ // If the problem was refcount overflow, we're technically violating the
+ // documented behaviour. This is OK; see comment in `<Account as Clone>::clone`.
+ refcount::Ref::null()
+ });
+ Participation {
+ aid,
+ pid,
+ cache,
+ tracker,
+ }
+ }
+}
+
+impl Drop for Participation {
+ fn drop(&mut self) {
+ (|| {
+ find_in_tracker! {
+ self.tracker => + tracker_strong, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?None
+ }
+ // release the cached claim
+ let from_cache = self.cache.take();
+ state.global.total_used.release(precord, from_cache);
+
+ if let Some(refcount::Garbage(mut removed)) =
+ slotmap_dec_ref!(&mut arecord.ps, self.pid.take(), &mut precord.refcount)
+ {
+ // We might not have called `release` on everything, so we do that here.
+ removed.auto_release(&mut state.global);
+ }
+ Some(())
+ })()
+ .unwrap_or_else(|| {
+ // Account or Participation or tracker destroyed.
+ // (This has no effect except in cfg(test), when it defuses the drop bombs)
+ self.pid.take().dispose_container_destroyed();
+ self.cache.take().dispose_participant_destroyed();
+ });
+ }
+}
+
+//========== impls on internal types ==========
+
+impl State {
+ /// Obtain all of the descendants of `parent_aid` according to the Child relation
+ ///
+ /// The returned `HashSet` includes `parent_aid`, its children,
+ /// their children, and so on.
+ ///
+ /// Used in the reclaimation algorithm in [`reclaim`].
+ fn get_aid_and_children_recursively(&self, parent_aid: AId) -> HashSet<AId> {
+ let mut out = HashSet::<AId>::new();
+ let mut queue: Vec<AId> = vec![parent_aid];
+ while let Some(aid) = queue.pop() {
+ let Some(arecord) = self.accounts.get(aid) else {
+ // shouldn't happen but no need to panic
+ continue;
+ };
+ if out.insert(aid) {
+ queue.extend(arecord.children.iter().cloned());
+ }
+ }
+ out
+ }
+}
+
+impl ARecord {
+ /// Release all memory that this account's participants claimed
+ fn auto_release(&mut self, global: &mut Global) {
+ for (_pid, mut precord) in self.ps.drain() {
+ precord.auto_release(global);
+ }
+ }
+}
+
+impl PRecord {
+ /// Release all memory that this participant claimed
+ fn auto_release(&mut self, global: &mut Global) {
+ let for_teardown = self.used.for_participant_teardown();
+ global.total_used.release(self, for_teardown);
+ }
+}
diff --git a/crates/tor-memquota/src/mtracker/bookkeeping.rs b/crates/tor-memquota/src/mtracker/bookkeeping.rs
new file mode 100644
index 000000000..ec1241196
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker/bookkeeping.rs
@@ -0,0 +1,308 @@
+//! Quantity bookkeeping
+//!
+//! Newtypes which wrap up a `Qty` (an amount of memory),
+//! and which assure proper accounting.
+//!
+//! Methods are provided for the specific transactions which are correct,
+//! in the accounting scheme in [`tracker`](super).
+//! So these types embody the data structure (fields and invariants) from `tracker`.
+//!
+//! # Panics
+//!
+//! In tests, these types panic if they are dropped when nonzero,
+//! if that's against the rules.
+
+use super::*;
+
+define_derive_deftly! {
+ /// Implement [`BookkeptQty`] and its supertraits
+ ///
+ /// By default, dropping when nonzero is forbidden,
+ /// and you must have a field `bomb: `[`DropBombCondition`].
+ /// `#[deftly(allow_nonzero_drop)]` suppresses this.
+ BookkeptQty =
+
+ ${defcond BOMB not(tmeta(allow_nonzero_drop))}
+
+ impl BookkeepableQty for $ttype {
+ const ZERO: $ttype = $ttype {
+ raw: Qty(0),
+ ${if BOMB {
+ bomb: DropBombCondition::new_armed(),
+ }}
+ };
+
+ fn as_raw(&self) -> Qty {
+ self.raw
+ }
+ }
+
+ impl<Rhs: BookkeepableQty> PartialEq<Rhs> for $ttype {
+ fn eq(&self, other: &Rhs) -> bool {
+ self.as_raw().eq(&other.as_raw())
+ }
+ }
+ impl<Rhs: BookkeepableQty> PartialOrd<Rhs> for $ttype {
+ fn partial_cmp(&self, other: &Rhs) -> Option<Ordering> {
+ self.as_raw().partial_cmp(&other.as_raw())
+ }
+ }
+
+ impl DefaultExtTake for $ttype {}
+
+ impl BookkeptQty for $ttype {
+ fn from_raw(q: Qty) -> Self {
+ $ttype {
+ raw: q,
+ ${if BOMB {
+ bomb: DropBombCondition::new_armed(),
+ }}
+ }
+ }
+ fn into_raw(mut self) -> Qty {
+ mem::replace(&mut self.raw, Qty(0))
+ }
+ }
+
+ assert_not_impl_any!($ttype: Clone, Into<Qty>, From<Qty>);
+
+ ${if BOMB {
+ #[cfg(test)]
+ impl Drop for $ttype {
+ fn drop(&mut self) {
+ drop_bomb_disarm_assert!(self.bomb, self.raw == Qty(0));
+ }
+ }
+ }}
+}
+
+/// Memory quantities that can work with bookkept quantities
+///
+/// This trait doesn't imply any invariants;
+/// it merely provides read-only access to the underlying value,
+/// and ways to make a zero.
+///
+/// Used by the derived `PartialEq` and `PartialOrd` impls on bookkept quantities.
+///
+/// Implemented by hand for `Qty`.
+///
+/// Implemented for bookkept types, along with `BookkeptQty`, by
+/// [`#[derive_deftly(BookKept)]`](derive_deftly_template_BookkeptQty).
+pub(super) trait BookkeepableQty: Default {
+ /// Zero (default value)
+ const ZERO: Self;
+
+ /// Inspect as a raw untracked Qty
+ fn as_raw(&self) -> Qty;
+}
+
+/// Bookkept memory quantities
+///
+/// Each bookkept quantity implements this trait,
+/// and has a single field `raw` of type `Qty`.
+///
+/// Should be Implemented by
+/// [`#[derive_deftly(BookKept)]`](derive_deftly_template_BookkeptQty)
+/// and for raw `Qty`.
+///
+/// # CORRECTNESS
+///
+/// All accesses to `raw`, or calls to `from_raw` or `into_raw`,
+/// should be made from transaction functions,
+/// which modify one or more bookkept quantities together,
+/// preserving the invariants.
+///
+/// `raw` may be accessed mutably by such functions, but a bookkept quantity type
+/// should be constructed only with `from_raw` and should not be moved out of.
+trait BookkeptQty: BookkeepableQty + DefaultExtTake {
+ /// Make a new bookkept quantity from a raw untracked Qty
+ ///
+ fn from_raw(q: Qty) -> Self;
+
+ /// Unwrap into a raw untracked Qty
+ fn into_raw(self) -> Qty;
+}
+
+impl BookkeepableQty for Qty {
+ const ZERO: Qty = Qty(0);
+
+ fn as_raw(&self) -> Qty {
+ *self
+ }
+}
+
+/// Total used, [`TotalQtyNotifier`].`total_used`, found in [`State`].`total_used`.
+///
+/// Can be "poisoned", preventing further claims.
+/// (We mark it poisoned if the reclamation task crashes,
+/// since in that situation we don't want to continue to use memory, unboundedly.)
+//
+// Poisoned is indicated by We setting to `MAX`.
+#[derive(Default, Debug, Deftly, derive_more::Display)]
+#[derive_deftly(BookkeptQty)]
+#[deftly(allow_nonzero_drop)] // Dropped only when the whole tracker is dropped
+pub(super) struct TotalQty {
+ /// See [`BookkeptQty`]
+ raw: Qty,
+}
+
+/// Qty used by a participant, found in [`PRecord`].`used`.
+///
+/// The tracker data structure has one of these for each Participant.
+///
+/// This is the total amount `claim`ed, plus the caches in each `Participation`.
+#[derive(Default, Debug, Deftly, derive_more::Display)]
+#[derive_deftly(BookkeptQty)]
+#[display(fmt = "{raw}")]
+pub(super) struct ParticipQty {
+ /// See [`BookkeptQty`]
+ raw: Qty,
+
+ /// See [`BookkeptQty`]
+ bomb: DropBombCondition,
+}
+
+/// "Cached" claim, on behalf of a Participant
+///
+/// Found in [`Participation`].`cache`,
+/// and accounted to the Participant (ie, included in `ParticipQty`).
+///
+/// Also used as a temporary variable in `claim()` and `release()` functions.
+/// When we return to the participant, outside the tracker, we
+/// essentially throw this away, since we don't give the caller any representation
+/// to store. The participant is supposed to track this separately somehow.
+#[derive(Default, Debug, Deftly, derive_more::Display)]
+#[derive_deftly(BookkeptQty)]
+#[display(fmt = "{raw}")]
+#[must_use]
+pub(super) struct ClaimedQty {
+ /// See [`BookkeptQty`]
+ raw: Qty,
+
+ /// See [`BookkeptQty`]
+ bomb: DropBombCondition,
+}
+
+impl TotalQty {
+ /// Claim a quantity, increasing the tracked amounts
+ ///
+ /// This module doesn't know anything about the memory quota,
+ /// so this doesn't do the quota check.
+ ///
+ /// The only caller is [`Participation::claim`].
+ pub(super) fn claim(&mut self, p_used: &mut ParticipQty, want: Qty) -> Option<ClaimedQty> {
+ // If poisoned, this add will fail (unless want is 0)
+ let new_self = self.raw.checked_add(*want)?;
+ if new_self == usize::MAX {
+ // This would poison us. If this happens, someone has gone mad, since
+ // we can't have allocated usize::MAX in total. We'll be reclaiming already.
+ // We don't want to poison ourselves in this situation. Hopefully the reclaim
+ // will collapse the errant participants.
+ return None;
+ }
+ let new_p_used = p_used.raw.checked_add(*want)?;
+ // commit
+ self.raw = Qty(new_self);
+ p_used.raw = Qty(new_p_used);
+ Some(ClaimedQty::from_raw(want))
+ }
+
+ /// Release a quantity, decreasing the tracked amounts
+ ///
+ /// (Handles underflow by saturating; returning an error is not going to be useful.)
+ pub(super) fn release(&mut self, p_used: &mut ParticipQty, have: ClaimedQty) {
+ let have = have.into_raw();
+ *p_used.raw = p_used.raw.saturating_sub(*have);
+
+ if self.raw != Qty::MAX {
+ // Don't unpoison
+ *self.raw = self.raw.saturating_sub(*have);
+ }
+ }
+
+ /// Declare this poisoned, and prevent further claims
+ pub(super) fn set_poisoned(&mut self) {
+ self.raw = Qty::MAX;
+ }
+}
+
+impl ClaimedQty {
+ /// Split a `ClaimedQty` into two `ClaimedQty`s
+ pub(super) fn split_off(&mut self, want: Qty) -> Option<ClaimedQty> {
+ let new_self = self.raw.checked_sub(*want)?;
+ // commit
+ *self.raw = new_self;
+ Some(ClaimedQty::from_raw(want))
+ }
+
+ /// Merge two `ClaimedQty`s
+ ///
+ /// (Handles overflow by saturating; returning an error is not going to be useful.)
+ pub(super) fn merge_into(&mut self, have: ClaimedQty) {
+ let have = have.into_raw();
+ *self.raw = self.raw.saturating_add(*have);
+ }
+
+ /// Obtain result for the participant, after having successfully recorded the amount claimed
+ ///
+ /// # CORRECTNESS
+ ///
+ /// This must be called only on a successful return path from [`Participation::claim`].
+ #[allow(clippy::unnecessary_wraps)] // returns Result; proves it's used on success path
+ pub(super) fn claim_return_to_participant(self) -> crate::Result<()> {
+ let _: Qty = self.into_raw();
+ Ok(())
+ }
+
+ /// When the participant indicates a release, enrol the amount in our bookkeping scheme
+ ///
+ /// Handles the quantity argument to [`Participation::release`].
+ ///
+ /// # CORRECTNESS
+ ///
+ /// This must be called only on entry to [`Participation::release`].
+ pub(super) fn release_got_from_participant(got: Qty) -> Self {
+ ClaimedQty::from_raw(got)
+ }
+
+ /// Dispose of a quantity that was claimed by a now-destroyed participant
+ ///
+ /// # CORRECTNESS
+ ///
+ /// The `ParticipQty` this was claimed from must also have been destroyed.
+ ///
+ /// So,
+ /// [`ParticipQty::for_participant_teardown`] and the corresponding
+ /// [`release`](TotalQty::release)
+ /// must have been called earlier - possibly, much earlier.
+ pub(super) fn dispose_participant_destroyed(mut self) {
+ let _: Qty = mem::take(&mut self).into_raw();
+ }
+}
+
+impl ParticipQty {
+ /// Prepare to destroy the `ParticipQty` in a participant that's being destroyed
+ ///
+ /// When the records of a participant that is being torn down are being destroyed,
+ /// we must remove our records of the memory that it allocated.
+ ///
+ /// This function is for that situation.
+ /// The returned `ClaimedQty` should then be passed to `release`.
+ ///
+ /// # CORRECTNESS
+ ///
+ /// The data structure where this `ParticipQty` resides
+ /// must be torn down (after we return).
+ ///
+ /// The `ClaimedQty` must be passed to [`TotalQty::release`],
+ /// passing the same `p_used`.
+ //
+ // We could provide this as a single transaction function, rather than requiring
+ // two calls. But the main code doesn't have a `TotalQty`, only a `TotalQtyNotifier`,
+ // so we'd need to add an additional passthrough method to `TotalQtyNotifier`,
+ // which doesn't seem worth it given that there's only one call site for this fn.
+ pub(super) fn for_participant_teardown(&self) -> ClaimedQty {
+ // We imagine that the Participant said it was releasing everything
+ ClaimedQty::from_raw(self.as_raw())
+ }
+}
diff --git a/crates/tor-memquota/src/mtracker/reclaim.rs b/crates/tor-memquota/src/mtracker/reclaim.rs
new file mode 100644
index 000000000..6e45b1f3c
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker/reclaim.rs
@@ -0,0 +1,396 @@
+//! Reclamation algorithm
+//!
+//! Implementation the of long-running [`task`] function,
+//! (which is the only export from here, the wider `mtracker` module).
+
+use super::*;
+
+mod deferred_drop;
+
+use deferred_drop::{DeferredDrop, GuardWithDeferredDrop};
+
+/// Total number of participants
+///
+/// Used in reporting and in calculations of various edge cases.
+/// On 64-bit systems, bigger than the refcounts, which are all `u32`
+type NumParticips = usize;
+
+//========== candiate victim analysis ==========
+
+/// The nominal data age of a participant
+#[derive(Ord, PartialOrd, Eq, PartialEq)]
+enum Age {
+ /// Treat this participant as having very old data
+ TreatAsVeryOld,
+ /// Data age value from the [`IsParticipant`]
+ Actual(CoarseInstant),
+}
+
+/// Participant status, as a candidate victim
+enum PStatus {
+ /// Treat participant as having data of age OldestData
+ Candidate(Age),
+ /// Tear this participant down right away
+ TearDown,
+ /// Treat participant as not having any data; don't reclaim from it
+ NoData,
+}
+
+/// Outcome of a completed reclamation run
+///
+/// This is used only within `choose_victim`, and only for logging
+#[derive(Debug, derive_more::Display)]
+enum Outcome {
+ /// We reached the low water mark
+ #[display(fmt = "complete")]
+ TargetReached,
+
+ /// We didn't, but we have so many participants that that's possibly expected
+ ///
+ /// (Can only happen on 32-bit platforms.)
+ #[display(fmt = "{} participants, good enough - stopping", n_particips)]
+ GoodEnough {
+ /// The number of participants
+ n_particips: NumParticips,
+ },
+}
+
+/// Figure out whether a participant is a candidate victim, and obtain its data age
+fn analyse_particip(precord: &PRecord, defer_drop: &mut DeferredDrop) -> PStatus {
+ let Some(particip) = precord.particip.upgrade() else {
+ // Oh! This participant has vanished!
+ // We can't reclaim from it. It may already be reclaiming.
+ // Delete it from our data structure.
+ return PStatus::TearDown;
+ };
+
+ let got_oldest = catch_unwind(AssertUnwindSafe(|| particip.get_oldest()));
+ defer_drop.push(particip);
+
+ match got_oldest {
+ Ok(Some(age)) => return PStatus::Candidate(Age::Actual(age)),
+ Ok(None) => {}
+ Err(_panicked) => {
+ // _panicked is of a useless type
+ error!("memory tracker: call to get_oldest panicked!");
+ return PStatus::TearDown;
+ }
+ }
+
+ // The participant claims not to have any memory
+ // There might be some cached, let's check
+
+ let Some(max_cached) = precord
+ .refcount
+ .as_usize()
+ .checked_mul(MAX_CACHE.as_usize())
+ else {
+ // WTF! So many Participation clones that the max usage has
+ // overflowed. (This can only happen on 32-bit platforms
+ // since refcount is a u32.) Probably we should reclaim
+ // from this participant.
+ log_ratelim!(
+ "memtrack: participant with many clones claims to have no data";
+ Err::<Void, _>(internal!("{} Participation clones", *precord.refcount));
+ );
+ return PStatus::Candidate(Age::TreatAsVeryOld);
+ };
+
+ if precord.used.as_raw() > Qty(max_cached) {
+ // This participant is lying to us somehow.
+ log_ratelim!(
+ "memtrack: participant claims to have no data, but our accounting disagrees";
+ Err::<Void, _>(internal!("{} used (by {} clones)", precord.used, *precord.refcount));
+ );
+ return PStatus::Candidate(Age::TreatAsVeryOld);
+ }
+
+ // Participant plausibly does have no data
+ PStatus::NoData
+}
+
+//========== reclamation algorith, the main pieces ==========
+
+/// State while reclamation is active
+struct Reclaiming {
+ /// The heap of candidates, oldest at top of heap
+ heap: BinaryHeap<Reverse<(Age, AId)>>,
+}
+
+/// A victim we have selected for reclamation
+///
+/// This designates a specific Participant.
+///
+/// But, note that we always reclaim from an Account, so if we are reclaiming
+/// from one `Victim`, we may be reclaiming from other `Victim`s with the same
+/// `AId` and different `IsParticipant`s. And because of inheritance, we might
+/// be reclaiming from other Accounts too.
+type Victim = (AId, drop_reentrancy::ProtectedArc<dyn IsParticipant>);
+
+/// Marker indicating that the victim's reclaim function panicked
+struct VictimPanicked;
+
+/// Set of responses from the victims, after they have all finished reclaiming.
+type VictimResponses = Vec<(AId, Result<Reclaimed, VictimPanicked>)>;
+
+impl Reclaiming {
+ /// Check to see if we should start reclaiming, and if so return a `Reclaiming`
+ ///
+ /// 1. Checks to see if usage is above `max`; if not, returns `None`
+ /// 2. Logs that we're starting reclamation
+ /// 3. Calculates the heap of data ages
+ fn maybe_start(state: &mut GuardWithDeferredDrop) -> Option<Self> {
+ let (state, deferred_drop) = state.deref_mut_both();
+
+ if *state.total_used <= state.global.config.max {
+ return None;
+ }
+
+ info!(
+ "memory tracking: {} > {}, reclamation started (target {})",
+ *state.total_used, state.config.max, state.config.low_water,
+ );
+
+ // `BinaryHeap` is a max heap, so use Rev
+ let mut heap = BinaryHeap::new();
+
+ // Build heap of participants we might want to reclaim from
+ // (and, while we're at it, tear down broken participants)
+ for (aid, arecord) in &mut state.accounts {
+ arecord.ps.retain(|_pid, precord| {
+ match analyse_particip(precord, deferred_drop) {
+ PStatus::Candidate(age) => {
+ heap.push(Reverse((age, aid)));
+ true // retain
+ }
+ PStatus::NoData => {
+ true // retain
+ }
+ PStatus::TearDown => {
+ precord.auto_release(&mut state.global);
+ false // remove
+ }
+ }
+ });
+ }
+
+ Some(Reclaiming { heap })
+ }
+
+ /// If we're reclaiming, choose the next victim(s) to reclaim
+ ///
+ /// This is the account whose participant has the oldest data age,
+ /// and all of that account's children.
+ ///
+ /// We might discover that we didn't want to continue reclamation after all:
+ /// this function is responsible for checking our progress
+ /// against the low water mark.
+ ///
+ /// If reclamation should stop, this function logs, and returns `None`.
+ fn choose_victims(&mut self, state: &mut State) -> Result<Option<Vec<Victim>>, ReclaimCrashed> {
+ let stop = |state: &mut State, outcome| {
+ info!(
+ "memory tracking reclamation reached: {} (target {}): {}",
+ *state.total_used, state.config.low_water, outcome,
+ );
+ Ok(None)
+ };
+
+ if *state.total_used <= state.config.low_water {
+ return stop(state, Outcome::TargetReached);
+ }
+ let Some(Reverse((_, oldest_aid))) = self.heap.pop() else {
+ // All our remaining participants are NoData.
+ let n_particips: usize = state
+ .accounts
+ .values()
+ .map(|ar| {
+ ar.ps
+ .values()
+ .map(
+ |pr| *pr.refcount as NumParticips, // refcount is u32, so this is fine
+ )
+ .sum::<NumParticips>()
+ })
+ .sum::<NumParticips>();
+
+ if state.total_used.as_raw().as_usize() / n_particips < usize::from(MAX_CACHE) {
+ // On 32-bit, this could happen due to the cache, if we have
+ // 2^32 / MAX_CACHE participants.
+ return stop(state, Outcome::GoodEnough { n_particips });
+ }
+
+ // Oh dear.
+ return Err(internal!(
+ "memory accounting state corrupted: used={} n_particips={} all NoData",
+ *state.total_used,
+ n_particips,
+ )
+ .into());
+ };
+
+ // When we do partial reclamation, rather than just Collapsing:
+ //
+ // fudge next_oldest by something to do with number of loop iterations,
+ // to avoid one-allocation-each-time ping pong between multiple caches
+ //
+ // (this match statement will fail to compile when we add a non-Collapsing variant)
+ //
+ // let next_oldest = heap.peek_lowest();
+ match None {
+ None | Some(Reclaimed::Collapsing) => {}
+ }
+
+ let victim_aids = state.get_aid_and_children_recursively(oldest_aid);
+
+ let victims: Vec<Victim> = {
+ let mut particips = vec![];
+ for aid in victim_aids {
+ let Some(arecord) = state.accounts.get_mut(aid) else {
+ // shouldn't happen but no need to panic
+ continue;
+ };
+ arecord.ps.retain(|_pid, precord| {
+ let Some(particip) = precord.particip.upgrade() else {
+ // tear this down!
+ precord.auto_release(&mut state.global);
+ return false;
+ };
+ particips.push((aid, particip));
+ true
+ });
+ }
+ particips
+ };
+
+ Ok(Some(victims))
+ }
+
+ /// Notify the chosen victims and obtain their responses
+ ///
+ /// This is the async part, and is done with the state unlocked.
+ // Doesn't actually need `self`, only `victims`, but we take it for form's sake
+ async fn notify_victims(&mut self, victims: Vec<Victim>) -> VictimResponses {
+ futures::future::join_all(
+ //
+ victims.into_iter().map(|(aid, particip)| async move {
+ let particip = particip.promise_dropping_is_ok();
+ // We run the `.reclaim()` calls within the same task (since that's what
+ // `join_all` does). So they all run on whatever executor thread is polling
+ // the reclamation task.
+ let reclaimed = AssertUnwindSafe(particip.reclaim())
+ .catch_unwind()
+ .await
+ .map_err(|_panicked| VictimPanicked);
+ // We drop the `ProtectedArc<dyn IsParticipant>` here, which is OK
+ // because we don't hold the lock. Since drop isn't async, and
+ // `join_all` doesn't spawn tasks, we drop them sequentially.
+ (aid, reclaimed)
+ }),
+ )
+ .await
+ }
+
+ /// Process the victim's responses and update `state` accordingly
+ // Doesn't actually need `self`, only `state`, but we take it for form's sake
+ fn handle_victim_responses(&mut self, state: &mut State, responses: VictimResponses) {
+ for (aid, reclaimed) in responses {
+ match reclaimed {
+ Ok(Reclaimed::Collapsing) | Err(VictimPanicked) => {
+ let Some(mut arecord) = state.accounts.remove(aid) else {
+ // Account is gone, fair enough
+ continue;
+ };
+ arecord.auto_release(&mut state.global);
+ // Account is definitely gone now
+ }
+ }
+ }
+ }
+}
+
+//========== the reclamation task, in terms of the pieces ==========-
+
+/// Return value from the task, when it finishes due to the tracker being shut down
+struct TaskFinished;
+
+/// Reclaim memory until we reach low water, if necessary
+///
+/// Looks to see if we're above `config.max`.
+/// If so, constructs a list of victims, and starts reclaiming from them,
+/// until we reach low water.
+async fn inner_loop(tracker: &Arc<MemoryQuotaTracker>) -> Result<(), ReclaimCrashed> {
+ let mut reclaiming;
+ let mut victims;
+ {
+ let mut state_guard = GuardWithDeferredDrop::new(tracker.lock()?);
+
+ let Some(r) = Reclaiming::maybe_start(&mut state_guard) else {
+ return Ok(());
+ };
+ reclaiming = r;
+
+ // Duplicating this call to reclaiming.choose_victims means we don't
+ // release the lock between `maybe_start` and `choose_victims` (here)
+ // and between `handle_victim_responses` and `choose_victims` (bellw).
+ // (Releasing the lock would not be a bug, but it's not desirable.)
+ let Some(v) = reclaiming.choose_victims(&mut state_guard)? else {
+ return Ok(());
+ };
+ victims = v;
+ }
+
+ loop {
+ let responses = reclaiming.notify_victims(mem::take(&mut victims)).await;
+ let mut state_guard = tracker.lock()?;
+ reclaiming.handle_victim_responses(&mut state_guard, responses);
+ let Some(v) = reclaiming.choose_victims(&mut state_guard)? else {
+ return Ok(());
+ };
+ victims = v;
+ }
+}
+
+/// Internal long-running task, handling reclamation - main loop
+///
+/// Handles routine logging, but not termination
+async fn task_loop(
+ tracker: &Weak<MemoryQuotaTracker>,
+ mut wakeup: mpsc::Receiver<()>,
+) -> Result<TaskFinished, ReclaimCrashed> {
+ loop {
+ // We don't hold a strong reference while we loop around, so we detect
+ // last drop of an actual client handle.
+ {
+ let Some(tracker) = tracker.upgrade() else {
+ return Ok(TaskFinished);
+ };
+
+ inner_loop(&tracker).await?;
+ }
+
+ let Some(()) = wakeup.next().await else {
+ // Sender dropped
+ return Ok(TaskFinished);
+ };
+ }
+}
+
+/// Internal long-running task, handling reclamation
+///
+/// This is the entrypoint used by the rest of the `tracker`.
+/// It handles logging of crashes.
+pub(super) async fn task(tracker: Weak<MemoryQuotaTracker>, wakeup: mpsc::Receiver<()>) {
+ match task_loop(&tracker, wakeup).await {
+ Ok(TaskFinished) => {}
+ Err(bug) => {
+ let _: Option<()> = (|| {
+ let tracker = tracker.upgrade()?;
+ let mut state = tracker.state.lock().ok()?;
+ state.total_used.set_poisoned();
+ Some(())
+ })();
+ error_report!(bug, "memory tracker task failed");
+ }
+ }
+}
diff --git a/crates/tor-memquota/src/mtracker/reclaim/deferred_drop.rs b/crates/tor-memquota/src/mtracker/reclaim/deferred_drop.rs
new file mode 100644
index 000000000..3b0742a82
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker/reclaim/deferred_drop.rs
@@ -0,0 +1,81 @@
+//! Deferred drop handling.
+//!
+//! We sometimes have `Arc<dyn Participant>`s we have obtained but don't want to drop yet
+//!
+//! See the top-level docs for context.
+//!
+//! When we drop the `Arc`, the refcount might become zero.
+//! Then the inner type would be dropped.
+//! The inner type is allowed to call back into us (for example, it may drop an `Account`).
+//! We must therefore not drop a caller's `Participant` with our own state lock held.
+//!
+//! This module has a helper type for assuring that we do defer drops.
+//
+// There are no separate tests for this module. Drop bombs are hard to test for,
+// and the rest of the code is just wrappers.
+
+use super::*;
+
+/// `MutexGuard<State>` but also a list of `Arc<dyn Partcipant>` to drop when we unlock
+#[derive(Debug, Default)]
+pub(super) struct GuardWithDeferredDrop<'m> {
+ /// The mutex guard
+ ///
+ /// Always `Some`; just an `Option` so we can move out during drop
+ guard: Option<MutexGuard<'m, State>>,
+
+ /// The participants we've acquired and which we want to drop later
+ deferred_drop: DeferredDrop,
+}
+
+/// Participants we've acquired and which we want to drop later, convenience alias
+pub(super) type DeferredDrop = Vec<drop_reentrancy::ProtectedArc<dyn IsParticipant>>;
+
+impl<'m> GuardWithDeferredDrop<'m> {
+ /// Prepare for handling deferred drops
+ pub(super) fn new(guard: MutexGuard<'m, State>) -> Self {
+ GuardWithDeferredDrop {
+ guard: Some(guard),
+ deferred_drop: vec![],
+ }
+ }
+
+ /// Obtain mutable borrows of the two components
+ pub(super) fn deref_mut_both(&mut self) -> (&mut State, &mut DeferredDrop) {
+ (
+ self.guard.as_mut().expect("deref_mut after drop"),
+ &mut self.deferred_drop,
+ )
+ }
+}
+
+impl Deref for GuardWithDeferredDrop<'_> {
+ type Target = State;
+ fn deref(&self) -> &State {
+ self.guard.as_ref().expect("deref after drop")
+ }
+}
+impl DerefMut for GuardWithDeferredDrop<'_> {
+ fn deref_mut(&mut self) -> &mut State {
+ self.deref_mut_both().0
+ }
+}
+
+// We use ProtectedArc. In tests, that has a drop bomb which requires us to
+// call `.promise_dropping_is_ok()`, on pain of panicking. So we must do that here.
+//
+// Outside tests, the normal drop order would be precisely correct:
+// the guard field comes first, so the compiler would drop it before the Arcs.
+// So we could make this `#[cfg(test)]` (and add some comments above about field order).
+// However, we prefer to use the same code, so that the correctness of
+// *production* GuardWithDeferredDrop is assured by the `ProtectedArc`.
+impl Drop for GuardWithDeferredDrop<'_> {
+ fn drop(&mut self) {
+ let guard = self.guard.take().expect("dropping twice!");
+ drop::<MutexGuard<_>>(guard);
+ // we just unlocked the guard, so drops that re-enter our code are fine
+ for p in self.deferred_drop.drain(..) {
+ p.promise_dropping_is_ok();
+ }
+ }
+}
diff --git a/crates/tor-memquota/src/mtracker/test.rs b/crates/tor-memquota/src/mtracker/test.rs
new file mode 100644
index 000000000..43d3dcd7c
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker/test.rs
@@ -0,0 +1,678 @@
+//! `tor_memtrack::tracker::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 @@ -->
+#![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
+
+use super::*;
+
+use std::collections::BTreeMap;
+use std::fmt::Write as _;
+use std::time::Duration;
+
+use itertools::Itertools;
+use rand::Rng;
+use slotmap::Key as _;
+use tracing_test::traced_test;
+
+use tor_basic_utils::RngExt as _;
+use tor_rtcompat::{CoarseDuration, CoarseTimeProvider as _, Runtime};
+use tor_rtmock::MockRuntime;
+
+//---------- useful utilities ----------
+
+fn secs(s: u64) -> CoarseDuration {
+ Duration::from_secs(s).into()
+}
+
+fn mby(mib: usize) -> usize {
+ mib * 1024 * 1024
+}
+
+fn mk_config() -> Config {
+ Config::builder()
+ .max(mby(20))
+ .low_water(mby(15))
+ .build()
+ .unwrap()
+}
+
+fn mk_tracker(rt: &impl Runtime) -> Arc<MemoryQuotaTracker> {
+ MemoryQuotaTracker::new(&rt, mk_config()).unwrap()
+}
+
+fn test_with_various_mocks<F, Fut>(f: F)
+where
+ F: Fn(tor_rtmock::MockRuntime) -> Fut,
+ Fut: Future<Output = ()>,
+{
+ MockRuntime::test_with_various(|rt| async {
+ // Make sure we can talk about times at least 1000s in the past
+ // TODO maybe this should be a feature of MockRuntime but what value to pick?
+ rt.advance_by(Duration::from_secs(1000)).await;
+ f(rt).await;
+ });
+}
+
+//---------- consistency check (test invariants against outside view) ----------
+
+use consistency::*;
+mod consistency {
+ use super::*;
+
+ #[derive(Default)]
+ pub(super) struct CallerInfoCollector {
+ g: usize,
+ acs: BTreeMap<AId, refcount::RawCount>,
+ pcs: BTreeMap<(AId, PId), (refcount::RawCount, usize)>,
+ debug_dump: String,
+ }
+
+ pub(super) trait HasCallerInfo {
+ fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector);
+ }
+
+ impl CallerInfoCollector {
+ pub(super) fn note_account(&mut self, acct: &Account, reclaimed: ReclaimedOrOk) {
+ writeln!(self.debug_dump, "acct {acct:?} {reclaimed:?}").unwrap();
+ if acct.aid.is_null() || reclaimed.is_err() {
+ return;
+ }
+ let ac = self.acs.entry(*acct.aid).or_default();
+ *ac += 1;
+ }
+ pub(super) fn note_particip(
+ &mut self,
+ p: &Participation,
+ reclaimed: ReclaimedOrOk,
+ used: usize,
+ ) {
+ writeln!(self.debug_dump, "particip {p:?} {reclaimed:?} {used:?}").unwrap();
+ if p.pid.is_null() || p.aid.is_null() || reclaimed.is_err() {
+ return;
+ }
+ self.note_partn_core(p, used);
+ }
+ pub(super) fn note_partn_clone(&mut self, p: &Participation) {
+ writeln!(self.debug_dump, "partn {p:?}").unwrap();
+ if p.pid.is_null() {
+ return;
+ }
+ self.note_partn_core(p, 0);
+ }
+ fn note_partn_core(&mut self, p: &Participation, x_used: usize) {
+ let pc = self.pcs.entry((p.aid, *p.pid)).or_default();
+ let used = *p.cache.as_raw() + x_used;
+ pc.0 += 1;
+ pc.1 += used;
+ self.g += used;
+ }
+ }
+
+ pub(super) fn check_consistency_general(
+ trk: &Arc<MemoryQuotaTracker>,
+ collect_caller_info: impl FnOnce(&mut CallerInfoCollector),
+ ) {
+ let state = trk.lock().unwrap();
+
+ let (expected, debug_dump) = {
+ let mut c = CallerInfoCollector::default();
+ collect_caller_info(&mut c);
+ ((c.g, c.acs, c.pcs), c.debug_dump)
+ };
+
+ let got = {
+ let mut gc = 0;
+ let mut acs = BTreeMap::new();
+ let mut pcs = BTreeMap::new();
+ for (aid, arecord) in &state.accounts {
+ acs.insert(aid, *arecord.refcount);
+ for (pid, precord) in &arecord.ps {
+ let used = *precord.used.as_raw();
+ gc += used;
+ pcs.insert((aid, pid), (*precord.refcount, used));
+ }
+ }
+
+ (gc, acs, pcs)
+ };
+
+ assert_eq!(
+ expected, got,
+ "\n----- dump (start) -----\n{debug_dump}----- dump (end) -----",
+ );
+ }
+}
+
+//---------- common test participant (state) ----------
+
+#[derive(Debug)]
+struct PartnState {
+ partn: Participation,
+ age: Option<CoarseInstant>,
+ used: usize,
+ reclaimed: ReclaimedOrOk,
+ show: String,
+}
+
+#[derive(Debug)]
+struct TestPartn {
+ state: Mutex<PartnState>,
+}
+
+impl TestPartn {
+ fn lock(&self) -> MutexGuard<PartnState> {
+ self.state.lock().unwrap()
+ }
+}
+
+impl From<PartnState> for TestPartn {
+ fn from(state: PartnState) -> TestPartn {
+ TestPartn {
+ state: Mutex::new(state),
+ }
+ }
+}
+
+impl TestPartn {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ self.lock().age
+ }
+ fn reclaim(&self) -> ReclaimFuture {
+ let () = mem::replace(&mut self.lock().reclaimed, Err(())).expect("reclaimed twice!");
+ Box::pin(async { Reclaimed::Collapsing })
+ }
+ fn is_reclaimed(&self) -> Result<(), ()> {
+ self.lock().reclaimed
+ }
+}
+
+impl IsParticipant for TestPartn {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ self.get_oldest()
+ }
+ fn reclaim(self: Arc<Self>) -> ReclaimFuture {
+ (*self.clone()).reclaim()
+ }
+}
+
+impl PartnState {
+ fn claim(&mut self, qty: usize) -> Result<(), crate::Error> {
+ claim_via(&mut self.partn, &self.show, &mut self.used, qty)
+ }
+
+ fn release(&mut self, qty: usize) {
+ release_via(&mut self.partn, &self.show, &mut self.used, qty);
+ }
+}
+
+fn claim_via(
+ via: &mut Participation,
+ show: impl Display,
+ used: &mut usize,
+ qty: usize,
+) -> Result<(), crate::Error> {
+ eprintln!("{show} claim {qty} {qty:#x}");
+ via.claim(qty)?;
+ *used += qty;
+ Ok(())
+}
+
+fn release_via(via: &mut Participation, show: impl Display, used: &mut usize, qty: usize) {
+ eprintln!("{show} release {qty} {qty:#x}");
+ via.release(qty);
+ *used -= qty;
+}
+
+impl HasCallerInfo for PartnState {
+ fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
+ collector.note_particip(&self.partn, self.reclaimed, self.used);
+ }
+}
+
+//---------- test participant which is directly the accountholder ----------
+
+#[derive(Debug, Deref)]
+struct UnifiedP {
+ acct: Account,
+ #[deref]
+ state: TestPartn,
+}
+
+type ReclaimedOrOk = Result<(), ()>;
+
+impl IsParticipant for UnifiedP {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ self.state.get_oldest()
+ }
+ fn reclaim(self: Arc<Self>) -> ReclaimFuture {
+ self.state.reclaim()
+ }
+}
+
+impl UnifiedP {
+ fn new(
+ rt: &impl Runtime,
+ trk: &Arc<MemoryQuotaTracker>,
+ parent: Option<&Account>,
+ age: CoarseDuration,
+ show: impl Display,
+ ) -> Arc<Self> {
+ let acct = trk.new_account(parent).unwrap();
+
+ let now = rt.now_coarse();
+
+ acct.register_participant_with(now, |partn| {
+ Ok::<_, Void>(Arc::new(UnifiedP {
+ acct: acct.clone(),
+ state: PartnState {
+ partn,
+ age: Some(now - age),
+ show: show.to_string(),
+ used: 0,
+ reclaimed: Ok(()),
+ }
+ .into(),
+ }))
+ })
+ .unwrap()
+ .void_unwrap()
+ }
+
+ async fn settle_check_consistency<'i>(
+ rt: &'i MockRuntime,
+ trk: &'i Arc<MemoryQuotaTracker>,
+ ups: impl IntoIterator<Item = &'i Arc<Self>> + 'i,
+ ) {
+ rt.advance_until_stalled().await;
+
+ check_consistency_general(trk, |collector| {
+ for up in ups {
+ up.note_consistency_caller_info(collector);
+ }
+ });
+ }
+}
+
+impl HasCallerInfo for UnifiedP {
+ fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
+ let state = self.lock();
+ collector.note_account(&self.acct, state.reclaimed);
+ state.note_consistency_caller_info(collector);
+ }
+}
+
+//---------- test cases with unified accountholder/participant ----------
+
+#[traced_test]
+#[test]
+fn basic() {
+ test_with_various_mocks(|rt| async move {
+ let trk = mk_tracker(&rt);
+
+ let ps: Vec<Arc<UnifiedP>> = (0..21)
+ .map(|i| UnifiedP::new(&rt, &trk, None, secs(i), i))
+ .collect();
+
+ for p in &ps[0..19] {
+ p.lock().claim(mby(1)).unwrap();
+ UnifiedP::settle_check_consistency(&rt, &trk, &ps).await;
+ }
+
+ let count_uncollapsed = || ps.iter().filter(|p| p.is_reclaimed().is_ok()).count();
+
+ assert_eq!(count_uncollapsed(), 21);
+
+ for p in &ps[20..] {
+ // check that we are exercising a situation with nonzero cached
+ // (this is set up by register_participant
+ assert_ne!(p.lock().partn.cache, Qty(0));
+
+ p.lock()
+ .claim(mby(1))
+ .expect("allocation rejected, during collapse, but collapse is async");
+ }
+
+ UnifiedP::settle_check_consistency(&rt, &trk, &ps).await;
+
+ assert_eq!(count_uncollapsed(), 14);
+
+ // Now we drop everything. This exercises much of the teardown!
+ });
+}
+
+#[traced_test]
+#[test]
+fn parent() {
+ test_with_various_mocks(|rt| async move {
+ for ages in [[10, 20], [20, 10]] {
+ eprintln!("ages: {ages:?}");
+ let [parent_age, child_age] = ages.map(secs);
+
+ let trk = mk_tracker(&rt);
+
+ let mk_p = |parent, age, show| UnifiedP::new(&rt, &trk, parent, age, show);
+
+ let parent = mk_p(None, parent_age, "parent");
+ parent.lock().claim(mby(7)).unwrap();
+ rt.advance_until_stalled().await;
+ assert!(parent.is_reclaimed().is_ok());
+
+ let child = mk_p(Some(&parent.acct), child_age, "child");
+ child.lock().claim(mby(7)).unwrap();
+ assert!(parent.is_reclaimed().is_ok());
+ assert!(child.is_reclaimed().is_ok());
+
+ let trigger = mk_p(None, secs(0), "trigger");
+ trigger.lock().claim(mby(7)).unwrap();
+ assert!(trigger.is_reclaimed().is_ok());
+
+ rt.advance_until_stalled().await;
+
+ if parent_age > child_age {
+ // parent is older than child, we're supposed to have reclaimed
+ // from the parent, causing reclamation of the child.
+ assert!(parent.is_reclaimed().is_err());
+ assert!(child.is_reclaimed().is_err());
+ } else {
+ // supposed to have reclaimed from child only
+ assert!(parent.is_reclaimed().is_ok());
+ assert!(child.is_reclaimed().is_err());
+ }
+ }
+ });
+}
+
+#[traced_test]
+#[test]
+fn cache() {
+ test_with_various_mocks(|rt| async move {
+ let seq = [
+ 1,
+ 1000,
+ *MAX_CACHE - 2000,
+ 3000,
+ *MAX_CACHE,
+ *MAX_CACHE - 1,
+ *MAX_CACHE + 1,
+ ];
+
+ let trk = mk_tracker(&rt);
+ let p = UnifiedP::new(&rt, &trk, None, secs(0), "p");
+
+ for qty in seq {
+ p.lock().claim(qty).unwrap();
+ UnifiedP::settle_check_consistency(&rt, &trk, [&p]).await;
+ }
+
+ for qty in seq {
+ p.lock().release(qty);
+ UnifiedP::settle_check_consistency(&rt, &trk, [&p]).await;
+ }
+
+ let mut p2 = p.lock().partn.clone();
+
+ let mut rng = tor_basic_utils::test_rng::Config::Deterministic.into_rng();
+ for _iter in 0..10_000 {
+ let qty = rng.gen_range_checked(0..=*MAX_CACHE).unwrap();
+ let p_use_i = rng.gen_range_checked(1..=3).unwrap();
+ {
+ let mut state = p.lock();
+ let state = &mut *state;
+
+ let mut p_use_buf;
+ let p_use = match p_use_i {
+ 1 => &mut state.partn,
+ 2 => &mut p2,
+ 3 => {
+ p_use_buf = p2.clone();
+ &mut p_use_buf
+ }
+ x => panic!("{}", x),
+ };
+
+ if rng.gen() || qty > state.used {
+ claim_via(p_use, p_use_i, &mut state.used, qty).unwrap();
+ } else {
+ release_via(p_use, p_use_i, &mut state.used, qty);
+ }
+ }
+
+ rt.advance_until_stalled().await;
+ check_consistency_general(&trk, |collector| {
+ p.note_consistency_caller_info(collector);
+ collector.note_partn_clone(&p2);
+ });
+ }
+ });
+}
+
+//---------- test client with multiple participants per account ----------
+
+#[derive(Debug)]
+struct ComplexAH {
+ acct: Account,
+ ps: Vec<Arc<TestPartn>>,
+}
+
+impl HasCallerInfo for ComplexAH {
+ fn note_consistency_caller_info(&self, collector: &mut CallerInfoCollector) {
+ let reclaimed = self
+ .ps
+ .iter()
+ .map(|p| p.lock().reclaimed)
+ .dedup()
+ .exactly_one()
+ .unwrap();
+
+ collector.note_account(&self.acct, reclaimed);
+ for p in &self.ps {
+ p.lock().note_consistency_caller_info(collector);
+ }
+ }
+}
+
+impl ComplexAH {
+ fn new(trk: &Arc<MemoryQuotaTracker>) -> Self {
+ ComplexAH {
+ acct: trk.new_account(None).unwrap(),
+ ps: vec![],
+ }
+ }
+
+ fn add_p(&mut self, now: CoarseInstant, age: CoarseDuration, show: impl Display) -> usize {
+ let cp = self
+ .acct
+ .register_participant_with(now, |partn| {
+ Ok::<_, Void>(Arc::new(TestPartn::from(PartnState {
+ partn,
+ age: Some(now - age),
+ show: show.to_string(),
+ used: 0,
+ reclaimed: Ok(()),
+ })))
+ })
+ .unwrap()
+ .void_unwrap();
+
+ let i = self.ps.len();
+ self.ps.push(cp);
+ i
+ }
+}
+
+#[traced_test]
+#[test]
+fn complex() {
+ test_with_various_mocks(|rt| async move {
+ let trk = mk_tracker(&rt);
+
+ let up = UnifiedP::new(&rt, &trk, None, secs(0), "U");
+ let mut ah = ComplexAH::new(&trk);
+ let now = rt.now_coarse();
+
+ for age in [5, 9] {
+ ah.add_p(now, secs(age), age);
+ }
+
+ let settle_check_consistency = || async {
+ rt.advance_until_stalled().await;
+
+ check_consistency_general(&trk, |collector| {
+ up.note_consistency_caller_info(collector);
+ ah.note_consistency_caller_info(collector);
+ });
+ };
+
+ up.lock().claim(mby(1)).unwrap();
+ ah.ps[0].lock().claim(mby(11)).unwrap();
+
+ settle_check_consistency().await;
+
+ assert!(up.is_reclaimed().is_ok());
+ for p in &ah.ps {
+ assert!(p.is_reclaimed().is_ok());
+ }
+
+ ah.ps[1].lock().claim(mby(11)).unwrap();
+
+ settle_check_consistency().await;
+ assert!(up.is_reclaimed().is_ok());
+ for p in &ah.ps {
+ assert!(p.is_reclaimed().is_err());
+ }
+ });
+}
+
+//---------- various error cases ----------
+
+#[derive(Debug)]
+struct DummyParticipant;
+
+impl IsParticipant for DummyParticipant {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ None
+ }
+ fn reclaim(self: Arc<Self>) -> ReclaimFuture {
+ Box::pin(async { Reclaimed::Collapsing })
+ }
+}
+
+#[traced_test]
+#[test]
+fn errors() {
+ test_with_various_mocks(|rt| async move {
+ let trk = mk_tracker(&rt);
+ let now = rt.now_coarse();
+
+ let mk_ah = || {
+ let mut ah = ComplexAH::new(&trk);
+ ah.add_p(now, secs(5), "p");
+ ah
+ };
+
+ const CLAIM: usize = MAX_CACHE.as_usize() + 1;
+
+ let dummy_dangling = || {
+ let p = Arc::new(DummyParticipant);
+ Arc::downgrade(&p)
+ // p dropped here
+ };
+ assert!(dummy_dangling().upgrade().is_none());
+
+ macro_rules! assert_error { { $error:ident, $r:expr } => {
+ let r = $r;
+ assert!(matches!(r, Err(Error::$error)), "unexpected: {:?} => {:?}", stringify!($r), &r);
+ } }
+
+ // Dropped account
+ {
+ let mut ah = mk_ah();
+ let wa1: WeakAccount = ah.acct.downgrade();
+ let p = ah.ps.pop().unwrap();
+ let wa2: WeakAccount = p.lock().partn.account();
+ drop(ah.acct);
+
+ rt.advance_until_stalled().await;
+ check_consistency_general(&trk, |_collector| ());
+
+ // account should be dead now
+ assert!(p.lock().claim(1).is_ok()); // from cache!
+ assert_error!(AccountClosed, p.lock().claim(CLAIM));
+ assert_error!(AccountClosed, wa1.upgrade());
+ assert_error!(AccountClosed, wa2.upgrade());
+
+ // but we can still release
+ p.lock().release(1);
+ }
+
+ // Dropped IsParticipant
+ {
+ let mut ah = mk_ah();
+ let p = ah.ps.pop().unwrap();
+ let mut state = Arc::into_inner(p).unwrap().state.into_inner().unwrap();
+
+ state.claim(mby(30)).unwrap(); // will trigger reclaim, which discovers the loss
+
+ rt.advance_until_stalled().await;
+ check_consistency_general(&trk, |collector| {
+ let reclaimed = Ok(()); // didn't manage to make the callback!
+ collector.note_account(&ah.acct, reclaimed);
+ });
+
+ assert_error!(ParticipantShutdown, state.claim(CLAIM));
+ }
+
+ // Reclaimed account
+ {
+ let ah = mk_ah();
+ ah.ps[0].lock().claim(mby(30)).unwrap();
+
+ rt.advance_until_stalled().await;
+ check_consistency_general(&trk, |_collector| ());
+
+ let p = &ah.ps[0];
+
+ assert!(p.lock().reclaimed.is_err());
+ assert_error!(AccountClosed, p.lock().claim(CLAIM));
+
+ let cloned = ah.acct.clone();
+ assert!(cloned.aid.is_null());
+ assert_error!(
+ AccountClosed,
+ ah.acct.register_participant(dummy_dangling())
+ );
+
+ let mut cloned = p.lock().partn.clone();
+ assert!(cloned.pid.is_null());
+ assert_error!(AccountClosed, cloned.claim(CLAIM));
+
+ // but we can still release
+ p.lock().release(1);
+ }
+
+ // Dropped tracker
+ {
+ let mut ah = mk_ah();
+ let p = ah.ps.pop().unwrap();
+ let wa = ah.acct.downgrade();
+ drop(ah.acct);
+ let _: MemoryQuotaTracker = Arc::into_inner(trk).unwrap();
+
+ assert_error!(TrackerShutdown, wa.upgrade());
+ assert_error!(TrackerShutdown, p.lock().partn.account().upgrade());
+ assert_error!(TrackerShutdown, p.lock().claim(CLAIM));
+ }
+ });
+}
diff --git a/crates/tor-memquota/src/mtracker/total_qty_notifier.rs b/crates/tor-memquota/src/mtracker/total_qty_notifier.rs
new file mode 100644
index 000000000..4c877ba9a
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker/total_qty_notifier.rs
@@ -0,0 +1,70 @@
+//! `TotalQtyNotifier`
+//!
+//! This newtype assures that we wake up the reclamation task when nceessary
+
+use super::*;
+
+/// Wrapper for `TotalQty`
+#[derive(Deref, Debug)]
+pub(super) struct TotalQtyNotifier {
+ /// Total memory usage
+ ///
+ /// Invariant: equal to
+ /// ```text
+ /// Σ Σ PRecord.used
+ /// ARecord PRecord
+ /// ```
+ #[deref]
+ total_used: TotalQty,
+
+ /// Condvar to wake up the reclamation task
+ ///
+ /// The reclamation task has another clone of this
+ reclamation_task_wakeup: mpsc::Sender<()>,
+}
+
+impl TotalQtyNotifier {
+ /// Make a new `TotalQtyNotifier`, which will notify a specified condvar
+ pub(super) fn new_zero(reclamation_task_wakeup: mpsc::Sender<()>) -> Self {
+ TotalQtyNotifier {
+ total_used: TotalQty::ZERO,
+ reclamation_task_wakeup,
+ }
+ }
+
+ /// Record that some memory has been (or will be) allocated by a participant
+ ///
+ /// Signals the wakeup task if we need to.
+ pub(super) fn claim(
+ &mut self,
+ precord: &mut PRecord,
+ want: Qty,
+ config: &Config,
+ ) -> crate::Result<ClaimedQty> {
+ let got = self
+ .total_used
+ .claim(&mut precord.used, want)
+ .ok_or_else(|| internal!("integer overflow attempting to add claim {}", want))?;
+ if self.total_used > config.max {
+ match self.reclamation_task_wakeup.try_send(()) {
+ Ok(()) => Ok(()),
+ Err(e) if e.is_full() => Ok(()),
+ Err(e) => Err(into_internal!("could not notify reclamation task!")(e)),
+ }?;
+ }
+ Ok(got)
+ }
+
+ /// Declare this poisoned, and prevent further claims
+ pub(super) fn set_poisoned(&mut self) {
+ self.total_used.set_poisoned();
+ }
+
+ /// Record that some memory has been (or will be) freed by a participant
+ pub(super) fn release(&mut self, precord: &mut PRecord, have: ClaimedQty) // infallible
+ {
+ // TODO if the participant's usage underflows, tell it to reclaim
+ // (and log some kind of internal error)
+ self.total_used.release(&mut precord.used, have);
+ }
+}
diff --git a/crates/tor-memquota/src/refcount.rs b/crates/tor-memquota/src/refcount.rs
new file mode 100644
index 000000000..cbe17b7f0
--- /dev/null
+++ b/crates/tor-memquota/src/refcount.rs
@@ -0,0 +1,315 @@
+//! Helpers for reference counting
+//!
+//! Two main purposes:
+//!
+//! * Consistent handling of overflow and underflow
+//! * Assurance of incrementing/decrementing as appropriate,
+//! including in combination with a slotmap containing the referenced data.
+//!
+//! The caller is responsible for making sure that the *right instance*'s
+//! [`Count`] is passed to the methods on [`Ref`].
+//
+// There are no separate tests for this module. Many of the tests would want to
+// exercise the `Ref`s drop bomb, which is troublesome since it's panic in drop,
+// which they're making Rust treat as an abort upstream.
+// (This scheme did detect a bug or two during development testing,
+// so the drop bomb is known to work.)
+//
+// Anyway, these functions are mostly newtype veneers over existing functionality.
+// They're tested by the MemoryQuotaTracker's tests.
+
+use crate::internal_prelude::*;
+
+/// Local alias for the counter type
+pub(crate) type RawCount = u32;
+
+/// Decrement a refcount and maybe remove a corresponding slotmap entry
+///
+/// ```rust,ignore
+/// fn slotmap_dec_ref!<K, V>(
+/// slotmap: &mut SlotMap<K, V>,
+/// ref_: Ref<K>,
+/// refcount: &mut Count<K>, // (typically) borrows from slotmap
+/// )
+/// ```
+//
+// This macro is a bit out-of-position, up here, because we want to be able to link
+// to it in our rustdocs.
+macro_rules! slotmap_dec_ref { { $slotmap:expr, $ref_:expr, $refcount:expr } => { { {
+ use $crate::refcount::*;
+ let key: Ref<_> = $ref_;
+ let refcount: &mut Count<_> = $refcount;
+ if let Some(Garbage(key)) = key.dispose(refcount) {
+ let slotmap: &mut SlotMap<_, _> = $slotmap;
+ let removed = slotmap.remove(key).expect("entry vanished or wrong key passed?!");
+ Some(Garbage(removed))
+ } else {
+ None
+ }
+} } } }
+
+/// A reference count, counting references with id type `K`
+#[derive(Default, Educe, Ord, PartialOrd, Eq, PartialEq, Deref)]
+#[educe(Debug)]
+pub(crate) struct Count<K> {
+ /// Actual count of references
+ #[deref]
+ count: RawCount,
+ /// Bind to the specific key type
+ // K is generally Send + Sync + 'static so we don't care about variance etc.
+ #[educe(Debug(ignore))]
+ marker: PhantomData<K>,
+}
+
+/// An copy of a [`slotmap::Key`] `K`, which is counted by a `RefCount`
+///
+/// Ie, a key of type `K` with the property that it
+/// keeps the refcounted data structure alive.
+///
+/// Must always be deleted using [`dispose`](Ref::dispose), not dropped.
+/// In tests, dropping a `RefCounted` will panic.
+///
+/// The `Default` value does *not* contribute to a reference count,
+/// and is fine to drop.
+#[derive(Deref, Educe)]
+#[educe(Debug, Default, Ord, Eq, PartialEq)]
+pub(crate) struct Ref<K: slotmap::Key> {
+ /// Actual key (without generics)
+ #[deref]
+ raw_key: K,
+ /// Bind to the specific key type
+ #[educe(Debug(ignore))]
+ marker: PhantomData<K>,
+ /// Drop bomb
+ ///
+ /// Also forces `Ref` not to be Clone
+ #[educe(Debug(ignore), Ord(ignore), Eq(ignore), PartialEq(ignore))]
+ #[allow(dead_code)]
+ bomb: DropBombCondition,
+}
+
+// educe's Ord is open-coded and triggers clippy::non_canonical_partial_ord_impl
+impl<K: slotmap::Key> PartialOrd for Ref<K> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+// Ideally we'd assert_not_impl on Ref but it has generics
+assert_not_impl_any!(DropBombCondition: Clone);
+
+/// Error: refcount overflowed
+#[derive(Debug, Clone, Error, Eq, PartialEq)]
+#[error("memory tracking refcount overflowed")]
+pub(crate) struct Overflow;
+
+/// Something which has become garbage
+///
+/// Often used within `Option`, for clarity. Examples:
+///
+/// * Key whose reference count has reached zero - see [`Ref::dispose`]
+/// * Value removed from a SlotMap - see [`slotmap_dec_ref!`]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub(crate) struct Garbage<K>(pub(crate) K);
+
+impl<K> Count<K> {
+ /// Make a new refcount with a specified value
+ const fn new_raw(count: RawCount) -> Self {
+ Count {
+ count,
+ marker: PhantomData,
+ }
+ }
+
+ /// Obtain this counter as a `usize`
+ ///
+ /// (Reference counts are `u32`, so this might be a conversion.)
+ pub(crate) fn as_usize(&self) -> usize {
+ // On a 16-bit platform this could theoretically overflow,
+ // but there would have to be >2^16 clones, which would be impossible.
+ let r: u32 = **self;
+ r as usize
+ }
+}
+
+/// Increment this refcount, but don't care about any [`Ref`]s
+fn inc_raw(c: &mut RawCount) -> Result<(), Overflow> {
+ *c = c.checked_add(1).ok_or(Overflow)?;
+ Ok(())
+}
+
+/// Decrement this refcount, but don't care about any [`Ref`]s
+///
+/// Returns [`Some(Garbage(()))`] if the count reached zero
+fn dec_raw(c: &mut RawCount) -> Option<Garbage<()>> {
+ *c = c
+ .checked_sub(1)
+ // if this happens, our data structure is corrupted, very bad
+ .expect("refcount underflow");
+ (*c == 0).then_some(Garbage(()))
+}
+
+impl<K: slotmap::Key> Ref<K> {
+ /// Create a refcounted reference `Ref` from an un-counted key, incrementing the count
+ pub(crate) fn new(key: K, count: &mut Count<K>) -> Result<Self, Overflow> {
+ inc_raw(&mut count.count)?;
+ Ok(Ref::from_raw(key))
+ }
+
+ /// Creates a null `Ref`, which doesn't refer to any slot (lookups always give `None`)
+ pub(crate) fn null() -> Self {
+ Ref::from_raw(K::null())
+ }
+
+ /// Internal function for creating a `Ref`
+ fn from_raw(raw_key: K) -> Self {
+ Ref {
+ raw_key,
+ marker: PhantomData,
+ bomb: DropBombCondition::new_armed(),
+ }
+ }
+
+ /// Dispose of a refcounted reference `Ref`, decrementing the count
+ ///
+ /// If the count reaches zero, the raw key is returned;
+ /// the caller should remove the corresponding data from the data structure.
+ pub(crate) fn dispose(mut self, refcount: &mut Count<K>) -> Option<Garbage<K>> {
+ let was = mem::take(&mut self.raw_key);
+ assert!(!was.is_null());
+ dec_raw(&mut refcount.count).map(|_: Garbage<()>| Garbage(was))
+ }
+
+ /// Dispose of a refcounted reference whose container no longer exists
+ ///
+ /// # CORRECTNESS
+ ///
+ /// This just forgets the reference, without decrementing any refcount.
+ /// If the container *does* still exist, a ref count ref will be leaked.
+ pub(crate) fn dispose_container_destroyed(mut self) {
+ let _: K = mem::take(&mut self.raw_key);
+ }
+}
+
+impl<K: slotmap::Key> DefaultExtTake for Ref<K> {}
+
+/// Insert a new entry into a slotmap using refcounted keys
+///
+/// `value_maker` should take the provided `Count`,
+/// and incorporate it into a new value.
+///
+/// On return, the entry will be in the map, and there will be one reference,
+/// which is returned.
+///
+/// There is no corresponding `slotmap_remove` in this module.
+/// Use [`Ref::dispose`] and handle any [`Garbage`] it returns.
+pub(crate) fn slotmap_insert<K: slotmap::Key, V>(
+ slotmap: &mut SlotMap<K, V>,
+ value_maker: impl FnOnce(Count<K>) -> V,
+) -> Ref<K> {
+ let (ref_, ()) = slotmap_try_insert(slotmap, move |refcount| {
+ Ok::<_, Void>((value_maker(refcount), ()))
+ })
+ .void_unwrap();
+ ref_
+}
+
+/// Insert a new entry into a slotmap using refcounted keys, fallibly and with extra data
+///
+/// Like [`slotmap_insert`] but:
+/// * `value_maker` can also return extra return data `RD` to the caller
+/// * `value_maker` is allowed to fail.
+///
+/// On successful return, the entry will be in the map, and
+/// the new `Ref` is returned along with the data `D`.
+pub(crate) fn slotmap_try_insert<K: slotmap::Key, V, E, RD>(
+ slotmap: &mut SlotMap<K, V>,
+ value_maker: impl FnOnce(Count<K>) -> Result<(V, RD), E>,
+) -> Result<(Ref<K>, RD), E> {
+ let refcount = Count::new_raw(1);
+ let (value, data) = value_maker(refcount)?;
+ let raw_key = slotmap.insert(value);
+ let ref_ = Ref {
+ raw_key,
+ marker: PhantomData,
+ bomb: DropBombCondition::new_armed(),
+ };
+ Ok((ref_, data))
+}
+
+#[cfg(test)]
+impl<K: slotmap::Key> Drop for Ref<K> {
+ fn drop(&mut self) {
+ drop_bomb_disarm_assert!(self.bomb, self.raw_key.is_null(),);
+ }
+}
+
+impl From<Overflow> for Error {
+ fn from(_overflow: Overflow) -> Error {
+ internal!("reference count overflow in memory tracking (out-of-control subsystem?)").into()
+ }
+}
+
+#[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 @@ -->
+ #![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
+
+ use super::*;
+
+ slotmap::new_key_type! {
+ struct Id;
+ }
+ #[derive(Eq, PartialEq, Debug)]
+ struct Record {
+ refcount: Count<Id>,
+ }
+ type Map = SlotMap<Id, Record>;
+
+ fn setup() -> (Map, Ref<Id>) {
+ let mut map = Map::default();
+ let ref_ = slotmap_insert(&mut map, |refcount| Record { refcount });
+ (map, ref_)
+ }
+
+ #[test]
+ fn good() {
+ let (mut map, ref1) = setup();
+
+ let ent = map.get_mut(*ref1).unwrap();
+ let ref2 = Ref::new(*ref1, &mut ent.refcount).unwrap();
+
+ let g1: Option<Garbage<Record>> = slotmap_dec_ref!(&mut map, ref1, &mut ent.refcount);
+ assert_eq!(g1, None);
+
+ let ent = map.get_mut(*ref2).unwrap();
+ let g2: Option<Garbage<Record>> = slotmap_dec_ref!(&mut map, ref2, &mut ent.refcount);
+ assert!(g2.is_some());
+ }
+
+ #[test]
+ fn try_insert_fail() {
+ let mut map = Map::default();
+ let () = slotmap_try_insert::<_, _, _, String>(&mut map, |_refcount| Err(())).unwrap_err();
+ }
+
+ #[test]
+ fn drop_ref_without_decrement() {
+ let (_map, mut ref1) = setup();
+ let h = ref1.bomb.make_simulated();
+ drop(ref1);
+ h.expect_exploded();
+ }
+}
diff --git a/crates/tor-memquota/src/utils.rs b/crates/tor-memquota/src/utils.rs
new file mode 100644
index 000000000..4540df03d
--- /dev/null
+++ b/crates/tor-memquota/src/utils.rs
@@ -0,0 +1,69 @@
+//! Miscellanous internal utilities
+
+use crate::internal_prelude::*;
+
+/// Quantity of memory used, measured in bytes.
+///
+/// Like `usize` but `Display`s in a more friendly and less precise way
+#[derive(Debug, Clone, Copy, Hash, Default, Eq, PartialEq, Ord, PartialOrd)] //
+#[derive(From, Into, Deref, DerefMut, Serialize, Deserialize)]
+#[serde(transparent)]
+pub(crate) struct Qty(pub(crate) usize);
+
+impl Qty {
+ /// Maximum for the type
+ pub(crate) const MAX: Qty = Qty(usize::MAX);
+
+ /// Return the value as a plain number, a `usize`
+ ///
+ /// Provided so call sites don't need to write an opaque `.0` everywhere,
+ /// even though that would be fine.
+ pub(crate) const fn as_usize(self) -> usize {
+ self.0
+ }
+}
+
+impl Display for Qty {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mb = self.0 as f32 / (1024. * 1024.);
+ write!(f, "{:.2}MiB", mb)
+ }
+}
+
+/// Convenience extension trait to provide `.take()`
+///
+/// Convenient way to provide `.take()` on some of our types.
+pub(crate) trait DefaultExtTake: Default {
+ /// Returns `*self`, replacing it with the default value.
+ fn take(&mut self) -> Self {
+ mem::take(self)
+ }
+}
+
+#[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::*;
+
+ #[test]
+ fn display_qty() {
+ let chk = |by, s| assert_eq!(Qty(by).to_string(), s);
+
+ chk(10 * 1024, "0.01MiB");
+ chk(1024 * 1024, "1.00MiB");
+ chk(1000 * 1024 * 1024, "1000.00MiB");
+ }
+}