summaryrefslogtreecommitdiff
path: root/crates/tor-memtrack/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/tor-memtrack/src')
-rw-r--r--crates/tor-memtrack/src/config.rs103
-rw-r--r--crates/tor-memtrack/src/drop_bomb.rs515
-rw-r--r--crates/tor-memtrack/src/drop_reentrancy.rs143
-rw-r--r--crates/tor-memtrack/src/error.rs144
-rw-r--r--crates/tor-memtrack/src/internal_prelude.rs63
-rw-r--r--crates/tor-memtrack/src/lib.rs206
-rw-r--r--crates/tor-memtrack/src/mtracker.rs1047
-rw-r--r--crates/tor-memtrack/src/mtracker/bookkeeping.rs308
-rw-r--r--crates/tor-memtrack/src/mtracker/reclaim.rs396
-rw-r--r--crates/tor-memtrack/src/mtracker/reclaim/deferred_drop.rs81
-rw-r--r--crates/tor-memtrack/src/mtracker/test.rs678
-rw-r--r--crates/tor-memtrack/src/mtracker/total_qty_notifier.rs70
-rw-r--r--crates/tor-memtrack/src/refcount.rs315
-rw-r--r--crates/tor-memtrack/src/utils.rs69
14 files changed, 0 insertions, 4138 deletions
diff --git a/crates/tor-memtrack/src/config.rs b/crates/tor-memtrack/src/config.rs
deleted file mode 100644
index ce8fd364c..000000000
--- a/crates/tor-memtrack/src/config.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-//! 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-memtrack/src/drop_bomb.rs b/crates/tor-memtrack/src/drop_bomb.rs
deleted file mode 100644
index 6a7b55fa6..000000000
--- a/crates/tor-memtrack/src/drop_bomb.rs
+++ /dev/null
@@ -1,515 +0,0 @@
-//! 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-memtrack/src/drop_reentrancy.rs b/crates/tor-memtrack/src/drop_reentrancy.rs
deleted file mode 100644
index 3e157cf88..000000000
--- a/crates/tor-memtrack/src/drop_reentrancy.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-//! 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-memtrack/src/error.rs b/crates/tor-memtrack/src/error.rs
deleted file mode 100644
index 4cca5bb10..000000000
--- a/crates/tor-memtrack/src/error.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-//! 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-memtrack/src/internal_prelude.rs b/crates/tor-memtrack/src/internal_prelude.rs
deleted file mode 100644
index c727496f4..000000000
--- a/crates/tor-memtrack/src/internal_prelude.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-//! 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-memtrack/src/lib.rs b/crates/tor-memtrack/src/lib.rs
deleted file mode 100644
index f6d5ad21b..000000000
--- a/crates/tor-memtrack/src/lib.rs
+++ /dev/null
@@ -1,206 +0,0 @@
-#![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-memtrack/src/mtracker.rs b/crates/tor-memtrack/src/mtracker.rs
deleted file mode 100644
index 00e276d35..000000000
--- a/crates/tor-memtrack/src/mtracker.rs
+++ /dev/null
@@ -1,1047 +0,0 @@
-//! Memory tracker, core and low-level API
-//!
-//! # Example
-//!
-//! ```
-//! use std::{collections::VecDeque, sync::{Arc, Mutex}};
-//! use tor_rtcompat::{CoarseInstant, CoarseTimeProvider, PreferredRuntime};
-//! use tor_memtrack::{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_memtrack::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-memtrack/src/mtracker/bookkeeping.rs b/crates/tor-memtrack/src/mtracker/bookkeeping.rs
deleted file mode 100644
index ec1241196..000000000
--- a/crates/tor-memtrack/src/mtracker/bookkeeping.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-//! 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-memtrack/src/mtracker/reclaim.rs b/crates/tor-memtrack/src/mtracker/reclaim.rs
deleted file mode 100644
index 6e45b1f3c..000000000
--- a/crates/tor-memtrack/src/mtracker/reclaim.rs
+++ /dev/null
@@ -1,396 +0,0 @@
-//! 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-memtrack/src/mtracker/reclaim/deferred_drop.rs b/crates/tor-memtrack/src/mtracker/reclaim/deferred_drop.rs
deleted file mode 100644
index 3b0742a82..000000000
--- a/crates/tor-memtrack/src/mtracker/reclaim/deferred_drop.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-//! 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-memtrack/src/mtracker/test.rs b/crates/tor-memtrack/src/mtracker/test.rs
deleted file mode 100644
index 43d3dcd7c..000000000
--- a/crates/tor-memtrack/src/mtracker/test.rs
+++ /dev/null
@@ -1,678 +0,0 @@
-//! `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-memtrack/src/mtracker/total_qty_notifier.rs b/crates/tor-memtrack/src/mtracker/total_qty_notifier.rs
deleted file mode 100644
index 4c877ba9a..000000000
--- a/crates/tor-memtrack/src/mtracker/total_qty_notifier.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-//! `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-memtrack/src/refcount.rs b/crates/tor-memtrack/src/refcount.rs
deleted file mode 100644
index cbe17b7f0..000000000
--- a/crates/tor-memtrack/src/refcount.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-//! 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-memtrack/src/utils.rs b/crates/tor-memtrack/src/utils.rs
deleted file mode 100644
index 4540df03d..000000000
--- a/crates/tor-memtrack/src/utils.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-//! 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");
- }
-}