summaryrefslogtreecommitdiff
path: root/crates/tor-memquota/src/mtracker.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/tor-memquota/src/mtracker.rs')
-rw-r--r--crates/tor-memquota/src/mtracker.rs1047
1 files changed, 1047 insertions, 0 deletions
diff --git a/crates/tor-memquota/src/mtracker.rs b/crates/tor-memquota/src/mtracker.rs
new file mode 100644
index 000000000..33333a930
--- /dev/null
+++ b/crates/tor-memquota/src/mtracker.rs
@@ -0,0 +1,1047 @@
+//! Memory quota tracker, core and low-level API
+//!
+//! # Example
+//!
+//! ```
+//! use std::{collections::VecDeque, sync::{Arc, Mutex}};
+//! use tor_rtcompat::{CoarseInstant, CoarseTimeProvider, PreferredRuntime};
+//! use tor_memquota::{mtracker, MemoryQuotaTracker, MemoryReclaimedError};
+//! use void::{ResultVoidExt, Void};
+//!
+//! #[derive(Debug)]
+//! struct TrackingQueue(Mutex<Result<Inner, MemoryReclaimedError>>);
+//! #[derive(Debug)]
+//! struct Inner {
+//! partn: mtracker::Participation,
+//! data: VecDeque<(Box<[u8]>, CoarseInstant)>,
+//! }
+//!
+//! impl TrackingQueue {
+//! fn push(&self, now: CoarseInstant, bytes: Box<[u8]>) -> Result<(), MemoryReclaimedError> {
+//! let mut inner = self.0.lock().unwrap();
+//! let inner = inner.as_mut().map_err(|e| e.clone())?;
+//! inner.partn.claim(bytes.len())?;
+//! inner.data.push_back((bytes, now));
+//! Ok(())
+//! }
+//! }
+//!
+//! impl mtracker::IsParticipant for TrackingQueue {
+//! fn get_oldest(&self) -> Option<CoarseInstant> {
+//! let inner = self.0.lock().unwrap();
+//! Some(inner.as_ref().ok()?.data.front()?.1)
+//! }
+//! fn reclaim(self: Arc<Self>) -> mtracker::ReclaimFuture {
+//! let mut inner = self.0.lock().unwrap();
+//! *inner = Err(MemoryReclaimedError::new());
+//! Box::pin(async { mtracker::Reclaimed::Collapsing })
+//! }
+//! }
+//!
+//! let runtime = PreferredRuntime::create().unwrap();
+//! let config = tor_memquota::Config::builder().max(1024*1024*1024).build().unwrap();
+//! let trk = MemoryQuotaTracker::new(&runtime, config).unwrap();
+//! let account = trk.new_account(None).unwrap();
+//!
+//! let queue: Arc<TrackingQueue> = account.register_participant_with(
+//! runtime.now_coarse(),
+//! |partn| {
+//! Ok::<_, Void>(Arc::new(TrackingQueue(Mutex::new(Ok(Inner {
+//! partn,
+//! data: VecDeque::new(),
+//! })))))
+//! },
+//! ).unwrap().void_unwrap();
+//!
+//! queue.push(runtime.now_coarse(), Box::new([0; 24])).unwrap();
+//! ```
+//
+// For key internal documentation about the data structure, see the doc comment for
+// `struct State` (down in the middle of the file).
+
+use crate::internal_prelude::*;
+
+mod bookkeeping;
+mod reclaim;
+mod total_qty_notifier;
+
+#[cfg(test)]
+mod test;
+
+use bookkeeping::{BookkeepableQty, ClaimedQty, ParticipQty, TotalQty};
+use total_qty_notifier::TotalQtyNotifier;
+
+/// Maximum amount we'll "cache" locally in a [`Participation`]
+///
+/// ie maximum value of `Participation.cache`.
+//
+// TODO is this a good amount? should it be configurable?
+pub(crate) const MAX_CACHE: Qty = Qty(16384);
+
+/// Target cache size when we seem to be claiming
+const TARGET_CACHE_CLAIMING: Qty = Qty(MAX_CACHE.as_usize() * 3 / 4);
+/// Target cache size when we seem to be releasing
+#[allow(clippy::identity_op)] // consistency
+const TARGET_CACHE_RELEASING: Qty = Qty(MAX_CACHE.as_usize() * 1 / 4);
+
+//---------- public data types ----------
+
+/// Memory data tracker
+///
+/// Instance of the memory quota system.
+///
+/// Usually found as `Arc<MemoryQuotaTracker>`.
+#[derive(Debug)]
+pub struct MemoryQuotaTracker {
+ /// The actual tracker state etc.
+ state: Mutex<State>,
+}
+
+/// Handle onto an Account
+///
+/// An `Account` is a handle. All clones refer to the same underlying conceptual Account.
+///
+/// `Account`s are created using [`MemoryQuotaTracker::new_account`].
+#[derive(Educe)]
+#[educe(Debug)]
+pub struct Account {
+ /// The account ID
+ aid: refcount::Ref<AId>,
+
+ /// The underlying tracker
+ #[educe(Debug(ignore))]
+ tracker: Arc<MemoryQuotaTracker>,
+}
+
+/// Weak handle onto an Account
+///
+/// Like [`Account`], but doesn't keep the account alive.
+/// Must be upgraded before use.
+//
+// Doesn't count for ARecord.account_clones
+//
+// We can't lift out Arc, so that the caller sees `Arc<Account>`,
+// because an Account is Arc<MemoryQuotaTracker> plus AId,
+// not Arc of something account-specific.
+#[derive(Clone, Educe)]
+#[educe(Debug)]
+pub struct WeakAccount {
+ /// The account ID
+ aid: AId,
+
+ /// The underlying tracker
+ #[educe(Debug(ignore))]
+ tracker: Weak<MemoryQuotaTracker>,
+}
+
+/// Handle onto a participant's participation in a tracker
+///
+/// `Participation` is a handle. All clones are for use by the same conceptual Participant.
+/// It doesn't keep the underlying Account alive.
+///
+/// `Participation`s are created by registering new participants,
+/// for example using [`Account::register_participant`].
+///
+/// Variables of this type are often named `partn`.
+#[derive(Debug)]
+pub struct Participation {
+ /// Participant id
+ pid: refcount::Ref<PId>,
+
+ /// Account id
+ aid: AId,
+
+ /// The underlying tracker
+ tracker: Weak<MemoryQuotaTracker>,
+
+ /// Quota we have preemptively claimed for use by this Account
+ ///
+ /// Has been added to `PRecord.used`,
+ /// but not yet returned by `Participation::claim`.
+ ///
+ /// This cache field arranges that most of the time we don't have to hammer a
+ /// single cache line.
+ ///
+ /// The value here is bounded by a configured limit.
+ ///
+ /// Invariants on memory accounting:
+ ///
+ /// * `Participation.cache < configured limit`
+ /// * `PRecord.used = Participation.cache + Σ Participation::claim - Σ P'n::release`
+ /// except if `PRecord` has been deleted
+ /// (ie when we aren't tracking any more and think the Participant is `Collapsing`).
+ /// * `Σ PRecord.used = State.total_used`
+ ///
+ /// Enforcement of these invariants is partially assured by
+ /// types in [`bookkeeping`].
+ cache: ClaimedQty,
+}
+
+/// Participants provide an impl of the hooks in this trait
+///
+/// Trait implemented by client of the memtrack API.
+///
+/// # Panic handling, "unwind safety"
+///
+/// If these methods panic, the memory tracker will tear down its records of the
+/// participant, preventing future allocations.
+///
+/// But, it's not guaranteed that these methods on `IsParticipant` won't be called again,
+/// even if they have already panicked on a previous occasion.
+/// Thus the implementations might see "broken invariants"
+/// as discussed in the docs for `std::panic::UnwindSafe`.
+///
+/// Nevertheless we don't make `RefUnwindSafe` a supertrait of `IsParticipant`.
+/// That would force the caller to mark *all* their methods unwind-safe,
+/// which is unreasonable (and probably undesirable).
+///
+/// Variables which are `IsParticipant` are often named `particip`.
+pub trait IsParticipant: Debug + Send + Sync + 'static {
+ /// Return the age of the oldest data held by this Participant
+ ///
+ /// `None` means this Participant holds no data.
+ ///
+ /// # Performance and reentrancy
+ ///
+ /// This function runs with the `MemoryQuotaTracker`'s internal global lock held.
+ /// Therefore:
+ ///
+ /// * It must be fast.
+ /// * it *must not* call back into methods from [`tracker`](crate::mtracker).
+ /// * It *must not* even `Clone` or `Drop` a [`MemoryQuotaTracker`],
+ /// [`Account`], or [`Participation`].
+ fn get_oldest(&self) -> Option<CoarseInstant>;
+
+ /// Start memory reclamation
+ ///
+ /// The Participant should start to free all of its memory,
+ /// and then return `Reclaimed::Collapsing`.
+ //
+ // In the future:
+ //
+ // Should free *at least* all memory at least as old as discard_...
+ //
+ // v1 of the actual implementation might not have `discard_everything_as_old_as`
+ // and `but_can_stop_discarding_...`,
+ // and might therefore only support Reclaimed::Collapsing
+ fn reclaim(
+ self: Arc<Self>,
+ // Future:
+ // discard_everything_as_old_as_this: RoughTime,
+ // but_can_stop_discarding_after_freeing_this_much: Qty,
+ ) -> ReclaimFuture;
+}
+
+/// Future returned by the [`IsParticipant::reclaim`] reclamation request
+pub type ReclaimFuture = Pin<Box<dyn Future<Output = Reclaimed> + Send + Sync>>;
+
+/// Outcome of [`IsParticipant::reclaim`]
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
+#[non_exhaustive]
+pub enum Reclaimed {
+ /// Participant is responding to reclamation by collapsing completely.
+ ///
+ /// All memory will be freed and `release`'d soon (if it hasn't been already).
+ /// `MemoryQuotaTracker` should forget the Participant and all memory it used, right away.
+ ///
+ /// Currently this is the only supported behaviour.
+ Collapsing,
+ // Future:
+ // /// Participant has now reclaimed some memory as instructed
+ // ///
+ // /// If this is not sufficient, tracker must call reclaim() again.
+ // /// (We may not want to implement Partial right away but the API
+ // /// ought to support it so let's think about it now, even if we don't implement it.)
+ // Partial,
+}
+
+//---------- principal data structure ----------
+
+slotmap::new_key_type! {
+ /// Identifies an Account
+ ///
+ /// After an account is torn down, the `AId` becomes invalid
+ /// and attempts to use it will give an error.
+ ///
+ /// The same `AId` won't be reused for a later Account.
+ struct AId;
+
+ /// Identifies a Participant within an Account
+ ///
+ /// Ie, PId is scoped within in the context of an account.
+ ///
+ /// As with `AId`, a `PId` is invalid after the
+ /// participation is torn down, and is not reused.
+ struct PId;
+}
+
+/// Memory tracker inner, including mutable state
+///
+/// # Module internal documentation
+///
+/// ## Data structure
+///
+/// * [`MemoryQuotaTracker`] contains mutex-protected `State`.
+/// * The `State` contains a [`SlotMap`] of account records [`ARecord`].
+/// * Each `ARecord` contains a `SlotMap` of participant records [`PRecord`].
+///
+/// The handles [`Account`], [`WeakAccount`], and [`Participation`],
+/// each contain a reference (`Arc`/`Weak`) to the `MemoryQuotaTracker`,
+/// and the necessary slotmap keys.
+///
+/// The `ARecord` and `PRecord` each contain a reference count,
+/// which is used to clean up when all the handles are gone.
+///
+/// The slotmap keys which count for the reference count (ie, strong references)
+/// are stored as [`refcount::Ref`],
+/// which helps assure correct reference counting.
+/// (Bare ids [`AId`] and [`PId`] are weak references.)
+///
+/// ## Data structure lookup
+///
+/// Given a reference to the tracker, and some ids, the macro `find_in_tracker!`
+/// is used to obtain mutable references to the `ARecord` and (if applicable) `PRecord`.
+///
+/// ## Bookkeeping
+///
+/// We use separate types for quantities of memory in various "states",
+/// rather than working with raw quantities.
+///
+/// The types, and the legitimate transactions, are in `bookkeeping`.
+///
+/// ## Reentrancy (esp. `Drop` and `Clone`)
+///
+/// When the handle structs are dropped or cloned, they must manipulate the refcount(s).
+/// So they must take the lock.
+/// Therefore, an `Account` and `Participation` may not be dropped with the lock held!
+///
+/// Internally, this is actually fairly straightforward:
+/// we take handles by reference, and constructors only make them at the last moment on return,
+/// so our internal code here, in this module, doesn't have owned handles.
+///
+/// We also need to worry about reentrantly reentering the tracker code, from user code.
+/// The user supplies a `dyn IsParticipant`.
+/// The principal methods are from [`IsParticipant`],
+/// for which we handle reentrancy in the docs.
+/// But we also implicitly invoke its `Drop` impl, which might in turn drop stuff of ours,
+/// such as [`Account`]s and [`Participation`]s, whose `Drop` impls need to take our lock.
+/// To make sure this isn't done reentrantly, we have a special newtype around it,
+/// and defer some of our drops during reclaim.
+/// That's in `drop_reentrancy` and `tracker::reclaim::deferred_drop`.
+///
+/// The `Debug` impl isn't of concern, since we don't call it ourselves.
+/// And we don't rely on it being `Clone`, since it's in an `Arc`.
+///
+/// ## Drop bombs
+///
+/// With `#[cfg(test)]`, several of our types have "drop bombs":
+/// they cause a panic if dropped inappropriately.
+/// This is intended to detect bad code paths during testing.
+#[derive(Debug, Deref, DerefMut)]
+struct State {
+ /// Global parts of state
+ ///
+ /// Broken out to allow passing both
+ /// `&mut Global` and `&mut ARecord`/`&mut PRecord`
+ /// to some function(s).
+ #[deref]
+ #[deref_mut]
+ global: Global,
+
+ /// Accounts
+ accounts: SlotMap<AId, ARecord>,
+}
+
+/// Global parts of `State`
+#[derive(Debug)]
+struct Global {
+ /// Total memory used
+ ///
+ /// Wrapper type for ensuring we wake up the reclaimation task
+ total_used: TotalQtyNotifier,
+
+ /// Configuration
+ config: Config,
+}
+
+/// Account record, within `State.accounts`
+#[derive(Debug)]
+#[must_use = "don't just drop, call auto_release"]
+struct ARecord {
+ /// Number of clones of `Account`; to know when to tear down the account
+ refcount: refcount::Count<AId>,
+
+ /// Child accounts
+ children: Vec<AId>,
+
+ /// Participants linked to this Account
+ ps: SlotMap<PId, PRecord>,
+}
+
+/// Participant record, within `ARecord.ps`
+#[derive(Debug)]
+#[must_use = "don't just drop, call auto_release"]
+struct PRecord {
+ /// Number of clones of `Participation`; to know when to tear down the participant
+ refcount: refcount::Count<PId>,
+
+ /// Memory usage of this participant
+ ///
+ /// Not 100% accurate, can lag, and be (boundedly) an overestimate
+ used: ParticipQty,
+
+ /// The hooks provided by the Participant
+ particip: drop_reentrancy::ProtectedWeak<dyn IsParticipant>,
+}
+
+//#################### IMPLEMENTATION ####################
+
+/// Given a `&Weak<MemoryQuotaTracker>`, find an account and maybe participant
+///
+/// ### Usage templates
+///
+/// ```rust,ignore
+/// find_in_tracker! {
+/// weak_tracker => + tracker, state;
+/// aid => arecord;
+/// [ pid => precord; ]
+/// [ ?Error | ?None ]
+/// };
+///
+/// find_in_tracker! {
+/// strong_tracker => state;
+/// .. // as above
+/// };
+/// ```
+///
+/// ### Input expressions (value arguments to the macro0
+///
+/// * `weak_tracker: &Weak<MemoryQuotaTracker>` (or equivalent)
+/// * `strong_tracker: &MemoryQuotaTracker` (or equivalent)
+/// * `aid: AId`
+/// * `pid: PId`
+///
+/// ### Generated bindings (identifier arguments to the macro)
+///
+/// * `tracker: Arc<MemoryQuotaTracker>`
+/// * `state: &mut State` (borrowed from a `MutexGuard<State>` borrowed from `tracker`)
+/// * `arecord: &mut ARecord` (mut borrowed from `state.accounts`)
+/// * `precord: &mut PRecord` (mut borrowed from `arecord.ps`)
+///
+/// There is no access to the `MutexGuard` itself.
+/// For control of the mutex release point, place `find_in_tracker!` in an enclosing block.
+///
+/// ### Error handling
+///
+/// If the tracker, account, or participant, can't be found,
+/// the macro returns early from the enclosing scope (using `?`).
+///
+/// If `Error` is specified, applies `?` to `Err(Error::...)`.
+/// If `None` is specified, just returns `None` (by applying `?` to None`).
+//
+// This has to be a macro because it makes a self-referential set of bindings.
+// Input syntax is a bit janky because macro_rules is so bad.
+// For an internal macro with ~9 call sites it's not worth making a big parsing contraption.
+macro_rules! find_in_tracker { {
+ // This `+` is needed because otherwise it's LL1-ambiguous and macro_rules can't cope
+ $tracker_input:expr => $( + $tracker:ident, )? $state:ident;
+ $aid:expr => $arecord:ident;
+ $( $pid:expr => $precord:ident; )?
+ // Either `Error` or None, to be passed to `find_in_tracker_eh!($eh ...: ...)`
+ // (We need this to be an un-repeated un-optional binding, because
+ // it is used within some other $( ... )?, and macro_rules gets confused.)
+ ? $eh:tt
+} => {
+ let tracker = &$tracker_input;
+ $(
+ let $tracker: Arc<MemoryQuotaTracker> = find_in_tracker_eh!(
+ $eh TrackerShutdown:
+ tracker.upgrade()
+ );
+ let tracker = &$tracker;
+ )?
+ let mut state: MutexGuard<State> = find_in_tracker_eh!(
+ $eh TrackerCorrupted:
+ tracker.state.lock().ok()
+ );
+ let $state: &mut State = &mut *state;
+ let aid: AId = $aid;
+ let $arecord: &mut ARecord = find_in_tracker_eh!(
+ $eh AccountClosed:
+ $state.accounts.get_mut(aid)
+ );
+ $(
+ let pid: PId = $pid;
+ let $precord: &mut PRecord = find_in_tracker_eh!(
+ $eh ParticipantShutdown:
+ $arecord.ps.get_mut(pid)
+ );
+ )?
+} }
+/// Error handling helper for `find_in_tracker`
+macro_rules! find_in_tracker_eh {
+ { None $variant:ident: $result:expr } => { $result? };
+ { Error $variant:ident: $result:expr } => { $result.ok_or(Error::$variant)? };
+}
+
+//========== impls on public types, including public methods and trait impls ==========
+
+//---------- MemoryQuotaTracker ----------
+
+impl MemoryQuotaTracker {
+ /// Set up a new `MemoryDataTracker`
+ pub fn new<R: Spawn>(runtime: &R, config: Config) -> Result<Arc<Self>, StartupError> {
+ let (reclaim_tx, reclaim_rx) = mpsc::channel(0 /* plus num_senders, ie 1 */);
+ let total_used = TotalQtyNotifier::new_zero(reclaim_tx);
+
+ let global = Global { total_used, config };
+ let accounts = SlotMap::default();
+ let state = Mutex::new(State { global, accounts });
+ let tracker = Arc::new(MemoryQuotaTracker { state });
+
+ // We don't provide a separate `launch_background_tasks`, because this task doesn't
+ // wake up periodically, or, indeed, do anything until the tracker is used.
+
+ let for_task = Arc::downgrade(&tracker);
+ runtime.spawn(reclaim::task(for_task, reclaim_rx))?;
+
+ Ok(tracker)
+ }
+
+ /// Make a new `Account`
+ ///
+ /// To actually record memory usage, a Participant must be added.
+ //
+ // Right now, parent can't be changed after construction of an Account,
+ // so circular accounts are impossible.
+ // But, we might choose to support that in the future.
+ // Circular parent relationships might need just a little care
+ // in the reclamation loop (to avoid infinitely looping),
+ // but aren't inherently unsupportable.
+ #[allow(clippy::redundant_closure_call)] // We have IEFEs for good reaons
+ pub fn new_account(self: &Arc<Self>, parent: Option<&Account>) -> crate::Result<Account> {
+ let mut state = self.lock()?;
+
+ let parent_aid_good = parent
+ .map(|parent| {
+ // Find and check the requested parent's Accountid
+
+ let parent_aid = *parent.aid;
+ let parent_arecord = state
+ .accounts
+ .get_mut(parent_aid)
+ .ok_or(Error::AccountClosed)?;
+
+ // Can we insert the new child without reallocating?
+ if !parent_arecord.children.spare_capacity_mut().is_empty() {
+ return Ok(parent_aid);
+ }
+
+ // No. Well, let's do some garbage collection.
+ // (Otherwise .children might grow without bound as accounts come and go)
+ //
+ // We would like to scan the accounts array while mutating this account.
+ // Instead, steal the children array temporarily and put the filtered one back.
+ // Must be infallible!
+ //
+ // The next line can't be in the closure (confuses borrowck)
+ let mut parent_children = mem::take(&mut parent_arecord.children);
+ (|| {
+ parent_children.retain(|child_aid| state.accounts.contains_key(*child_aid));
+
+ // Put the filtered list back, so sanity is restored.
+ state
+ .accounts
+ .get_mut(parent_aid)
+ .expect("parent vanished!")
+ .children = parent_children;
+ })();
+
+ Ok::<_, Error>(parent_aid)
+ })
+ .transpose()?;
+
+ // We have resolved the parent AId and prepared to add the new account to its list of
+ // children. We still hold the lock, so nothing can have changed.
+
+ // commitment - infallible IEFE assures that so we don't do half of it
+ Ok((|| {
+ let aid = refcount::slotmap_insert(&mut state.accounts, |refcount| ARecord {
+ refcount,
+ children: vec![],
+ ps: SlotMap::default(),
+ });
+
+ if let Some(parent_aid_good) = parent_aid_good {
+ state
+ .accounts
+ .get_mut(parent_aid_good)
+ .expect("parent vanished!")
+ .children
+ .push(*aid);
+ }
+
+ let tracker = self.clone();
+ Account { aid, tracker } // don't make this fallible, see above.
+ })())
+ }
+
+ /// Obtain the lock on the state
+ fn lock(&self) -> Result<MutexGuard<State>, TrackerCorrupted> {
+ Ok(self.state.lock()?)
+ }
+}
+
+//---------- Account ----------
+
+impl Account {
+ /// Register a new Participant
+ ///
+ /// Returns the [`Participation`], which can be used to record memory allocations.
+ ///
+ /// Often, your implementation of [`IsParticipant`] wants to contain the [`Participation`].
+ /// If so, use [`register_participant_with`](Account::register_participant_with) instead.
+ pub fn register_participant(
+ &self,
+ particip: Weak<dyn IsParticipant>,
+ ) -> Result<Participation, Error> {
+ let aid = *self.aid;
+ find_in_tracker! {
+ self.tracker => state;
+ aid => arecord;
+ ?Error
+ }
+
+ let (pid, cache) = refcount::slotmap_try_insert(&mut arecord.ps, |refcount| {
+ let mut precord = PRecord {
+ refcount,
+ used: ParticipQty::ZERO,
+ particip: drop_reentrancy::ProtectedWeak::new(particip),
+ };
+ let cache =
+ state
+ .global
+ .total_used
+ .claim(&mut precord, MAX_CACHE, &state.global.config)?;
+ Ok::<_, Error>((precord, cache))
+ })?;
+
+ let tracker = Arc::downgrade(&self.tracker);
+ Ok(Participation {
+ tracker,
+ pid,
+ aid,
+ cache,
+ })
+ }
+
+ /// Set the callbacks for a Participant (identified by its weak ids)
+ fn set_participant_callbacks(
+ &self,
+ aid: AId,
+ pid: PId,
+ particip: drop_reentrancy::ProtectedWeak<dyn IsParticipant>,
+ ) -> Result<(), Error> {
+ find_in_tracker! {
+ self.tracker => state;
+ aid => arecord;
+ pid => precord;
+ ?Error
+ }
+ precord.particip = particip;
+ Ok(())
+ }
+
+ /// Register a new Participant using a constructor
+ ///
+ /// Passes `constructor` a [`Participation`] for the nascent Participant.
+ /// Returns the `P: IsParticipant` provided by the constructor.
+ ///
+ /// For use when your `impl `[`IsParticipant`] wants to own the `Participation`.
+ ///
+ /// # Re-entrancy guarantees
+ ///
+ /// The `Participation` *may* be used by `constructor` for claiming memory use,
+ /// even during construction.
+ /// `constructor` may also clone the `Participation`, etc.
+ ///
+ /// Reclamation callbacks (via the `P as IsParticipant` impl) cannot occur
+ /// until `constructor` returns.
+ ///
+ /// # Error handling
+ ///
+ /// Failures can occur before `constructor` is called,
+ /// or be detected afterwards.
+ /// If a failure is detected after `constructor` returns,
+ /// the `Arc<P>` from `constructor` will be dropped
+ /// (resulting in `P` being dropped, unless `constructor` kept another clone of it).
+ ///
+ /// `constructor` may also fail (throwing a different error type, `E`),
+ /// in which case `register_participant_with` returns `Ok(Err(E))`.
+ ///
+ /// On successful setup of the Participant, returns `Ok(Ok(Arc<P>))`.
+ pub fn register_participant_with<P: IsParticipant, E>(
+ &self,
+ now: CoarseInstant,
+ constructor: impl FnOnce(Participation) -> Result<Arc<P>, E>,
+ ) -> Result<Result<Arc<P>, E>, Error> {
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ /// Temporary participant, which stands in during constructon
+ #[derive(Debug)]
+ struct TemporaryParticipant {
+ /// The age, which is right now. We hope this is all fast!
+ now: CoarseInstant,
+ /// Did someone call reclaim() ?
+ collapsing: AtomicBool,
+ }
+
+ impl IsParticipant for TemporaryParticipant {
+ fn get_oldest(&self) -> Option<CoarseInstant> {
+ Some(self.now)
+ }
+ fn reclaim(self: Arc<Self>) -> ReclaimFuture {
+ self.collapsing.store(true, Ordering::Release);
+ Box::pin(async { Reclaimed::Collapsing })
+ }
+ }
+
+ let temp_particip = Arc::new(TemporaryParticipant {
+ now,
+ collapsing: false.into(),
+ });
+
+ let partn = self.register_participant(Arc::downgrade(&temp_particip) as _)?;
+ let aid = partn.aid;
+ let pid_weak = *partn.pid;
+
+ // We don't hold the state lock here. register_participant took it and released it.
+ // This is important, because the constructor might call claim!
+ // (And, also, we don't want the constructor panicking to poison the whole tracker.)
+ // But it means there can be quite a lot of concurrent excitement,
+ // including, theoretically, a possible reclaim.
+ let particip = match constructor(partn) {
+ Ok(y) => y,
+ Err(e) => return Ok(Err(e)),
+ };
+ let particip = drop_reentrancy::ProtectedArc::new(particip);
+
+ // IEFE prevents use from accidentally dropping `particip` until we mean to
+ let r = (|| {
+ let weak = {
+ let weak = particip.downgrade();
+
+ // Trait cast, from Weak<P> to Weak<dyn IsParticipant>.
+ // We can only do this for a primitive, so we must unprotect
+ // the Weak, converr it, and protect it again.
+ drop_reentrancy::ProtectedWeak::new(weak.unprotect() as _)
+ };
+ self.set_participant_callbacks(aid, pid_weak, weak)?;
+
+ if temp_particip.collapsing.load(Ordering::Acquire) {
+ return Err(Error::ParticipantShutdown);
+ }
+ Ok(())
+ })();
+
+ let particip = particip.promise_dropping_is_ok();
+ r?;
+ Ok(Ok(particip))
+ }
+
+ /// Obtains a handle for the `MemoryQuotaTracker`
+ pub fn tracker(&self) -> Arc<MemoryQuotaTracker> {
+ self.tracker.clone()
+ }
+
+ /// Downgrade to a weak handle for the same Account
+ pub fn downgrade(&self) -> WeakAccount {
+ WeakAccount {
+ aid: *self.aid,
+ tracker: Arc::downgrade(&self.tracker),
+ }
+ }
+}
+
+impl Clone for Account {
+ fn clone(&self) -> Account {
+ let tracker = self.tracker.clone();
+ let aid = (|| {
+ let aid = *self.aid;
+ find_in_tracker! {
+ tracker => state;
+ aid => arecord;
+ ?None
+ }
+ let aid = refcount::Ref::new(aid, &mut arecord.refcount).ok()?;
+ // commitment point
+ Some(aid)
+ })()
+ .unwrap_or_else(|| {
+ // Either the account has been closed, or our refcount overflowed.
+ // Return a busted `Account`, which always fails when we try to use it.
+ //
+ // If the problem was a refcount overflow, we're technically violating the
+ // documented behaviour, since the returned `Account` isn't equivalent
+ // to the original. We could instead choose to tear down the Account;
+ // that would be legal; but it's a lot of code to marginally change the
+ // behaviour for a very unlikely situation.
+ refcount::Ref::null()
+ });
+ Account { aid, tracker }
+ }
+}
+
+impl Drop for Account {
+ fn drop(&mut self) {
+ (|| {
+ find_in_tracker! {
+ self.tracker => state;
+ *self.aid => arecord;
+ ?None
+ }
+ if let Some(refcount::Garbage(mut removed)) =
+ slotmap_dec_ref!(&mut state.accounts, self.aid.take(), &mut arecord.refcount)
+ {
+ // This account is gone. Automatically release everything.
+ removed.auto_release(state);
+ }
+ Some(())
+ })()
+ .unwrap_or_else(|| {
+ // Account has been torn down. Dispose of the strong ref.
+ // (This has no effect except in cfg(test), when it defuses the drop bombs)
+ self.aid.take().dispose_container_destroyed();
+ });
+ }
+}
+
+//---------- WeakAccount ----------
+
+impl WeakAccount {
+ /// Upgrade to an `Account`, if the account still exists
+ pub fn upgrade(&self) -> crate::Result<Account> {
+ let aid = self.aid;
+ // (we must use a block, and can't use find_in_tracker's upgrade, because borrowck)
+ let tracker = self.tracker.upgrade().ok_or(Error::TrackerShutdown)?;
+ let aid = {
+ find_in_tracker! {
+ tracker => state;
+ aid => arecord;
+ ?Error
+ }
+ refcount::Ref::new(aid, &mut arecord.refcount)?
+ // commitment point
+ };
+ Ok(Account { aid, tracker })
+ }
+
+ /// Obtains a handle onto the `MemoryQuotaTracker`
+ ///
+ /// The returned handle is itself weak, and needs to be upgraded before use.
+ pub fn tracker(&self) -> Weak<MemoryQuotaTracker> {
+ self.tracker.clone()
+ }
+}
+
+//---------- Participation ----------
+
+impl Participation {
+ /// Record that some memory has been (or will be) allocated
+ pub fn claim(&mut self, want: usize) -> crate::Result<()> {
+ self.claim_qty(Qty(want))
+ }
+
+ /// Record that some memory has been (or will be) allocated (using `Qty`)
+ pub(crate) fn claim_qty(&mut self, want: Qty) -> crate::Result<()> {
+ if let Some(got) = self.cache.split_off(want) {
+ return got.claim_return_to_participant();
+ }
+
+ find_in_tracker! {
+ self.tracker => + tracker, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?Error
+ };
+
+ let mut claim = |want| -> Result<ClaimedQty, _> {
+ state
+ .global
+ .total_used
+ .claim(precord, want, &state.global.config)
+ };
+ let got = claim(want)?;
+
+ if want <= TARGET_CACHE_CLAIMING {
+ // While we're here, fill the cache to TARGET_CACHE_CLAIMING.
+ // Cannot underflow: cache < want (since we failed at `got` earlier
+ // and we've just checked want <= TARGET_CACHE_CLAIMING.
+ let want_more_cache = Qty(*TARGET_CACHE_CLAIMING - *self.cache.as_raw());
+ if let Ok(add_cache) = claim(want_more_cache) {
+ // On error, just don't do this; presumably the error will show up later
+ // (we mustn't early exit here, because we've got the claim in our hand).
+ self.cache.merge_into(add_cache);
+ }
+ }
+ got.claim_return_to_participant()
+ }
+
+ /// Record that some memory has been (or will be) freed by a participant
+ pub fn release(&mut self, have: usize) // infallible
+ {
+ self.release_qty(Qty(have));
+ }
+
+ /// Record that some memory has been (or will be) freed by a participant (using `Qty`)
+ pub(crate) fn release_qty(&mut self, have: Qty) // infallible
+ {
+ let have = ClaimedQty::release_got_from_participant(have);
+ self.cache.merge_into(have);
+ if self.cache > MAX_CACHE {
+ match (|| {
+ find_in_tracker! {
+ self.tracker => + tracker, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?None
+ }
+ let return_from_cache = Qty(*self.cache.as_raw() - *TARGET_CACHE_RELEASING);
+ let from_cache = self.cache.split_off(return_from_cache).expect("impossible");
+ state.global.total_used.release(precord, from_cache);
+ Some(())
+ })() {
+ Some(()) => {} // we've given our cache back to the tracker
+ None => {
+ // account (or whole tracker!) is gone
+ // throw away the cache so that we don't take this path again for a bit
+ self.cache.take().dispose_participant_destroyed();
+ }
+ }
+ }
+ }
+
+ /// Obtain a handle onto the account
+ ///
+ /// The returned handle is weak, and needs to be upgraded before use,
+ /// since a [`Participation`] doesn't keep its Account alive.
+ ///
+ /// The returned `WeakAccount` is equivalent to
+ /// all the other account handles for the same account.
+ pub fn account(&self) -> WeakAccount {
+ WeakAccount {
+ aid: self.aid,
+ tracker: self.tracker.clone(),
+ }
+ }
+}
+
+impl Clone for Participation {
+ fn clone(&self) -> Participation {
+ let aid = self.aid;
+ let cache = ClaimedQty::ZERO;
+ let tracker: Weak<_> = self.tracker.clone();
+ let pid = (|| {
+ let pid = *self.pid;
+ find_in_tracker! {
+ self.tracker => + tracker_strong, state;
+ aid => _arecord;
+ pid => precord;
+ ?None
+ }
+ let pid = refcount::Ref::new(pid, &mut precord.refcount).ok()?;
+ // commitment point
+ Some(pid)
+ })()
+ .unwrap_or_else(|| {
+ // The account has been closed, the participant torn down, or the refcount
+ // overflowed. We can a busted `Participation`.
+ //
+ // We *haven't* incremented the refcount, so we mustn't return pid as a strong
+ // reference. We aren't supposed to count towards PRecord.refcount, we we *can*
+ // return the weak reference aid. (`refcount` type-fu assures this is correct.)
+ //
+ // If the problem was refcount overflow, we're technically violating the
+ // documented behaviour. This is OK; see comment in `<Account as Clone>::clone`.
+ refcount::Ref::null()
+ });
+ Participation {
+ aid,
+ pid,
+ cache,
+ tracker,
+ }
+ }
+}
+
+impl Drop for Participation {
+ fn drop(&mut self) {
+ (|| {
+ find_in_tracker! {
+ self.tracker => + tracker_strong, state;
+ self.aid => arecord;
+ *self.pid => precord;
+ ?None
+ }
+ // release the cached claim
+ let from_cache = self.cache.take();
+ state.global.total_used.release(precord, from_cache);
+
+ if let Some(refcount::Garbage(mut removed)) =
+ slotmap_dec_ref!(&mut arecord.ps, self.pid.take(), &mut precord.refcount)
+ {
+ // We might not have called `release` on everything, so we do that here.
+ removed.auto_release(&mut state.global);
+ }
+ Some(())
+ })()
+ .unwrap_or_else(|| {
+ // Account or Participation or tracker destroyed.
+ // (This has no effect except in cfg(test), when it defuses the drop bombs)
+ self.pid.take().dispose_container_destroyed();
+ self.cache.take().dispose_participant_destroyed();
+ });
+ }
+}
+
+//========== impls on internal types ==========
+
+impl State {
+ /// Obtain all of the descendants of `parent_aid` according to the Child relation
+ ///
+ /// The returned `HashSet` includes `parent_aid`, its children,
+ /// their children, and so on.
+ ///
+ /// Used in the reclaimation algorithm in [`reclaim`].
+ fn get_aid_and_children_recursively(&self, parent_aid: AId) -> HashSet<AId> {
+ let mut out = HashSet::<AId>::new();
+ let mut queue: Vec<AId> = vec![parent_aid];
+ while let Some(aid) = queue.pop() {
+ let Some(arecord) = self.accounts.get(aid) else {
+ // shouldn't happen but no need to panic
+ continue;
+ };
+ if out.insert(aid) {
+ queue.extend(arecord.children.iter().cloned());
+ }
+ }
+ out
+ }
+}
+
+impl ARecord {
+ /// Release all memory that this account's participants claimed
+ fn auto_release(&mut self, global: &mut Global) {
+ for (_pid, mut precord) in self.ps.drain() {
+ precord.auto_release(global);
+ }
+ }
+}
+
+impl PRecord {
+ /// Release all memory that this participant claimed
+ fn auto_release(&mut self, global: &mut Global) {
+ let for_teardown = self.used.for_participant_teardown();
+ global.total_used.release(self, for_teardown);
+ }
+}