//! 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 PartialEq for $ttype { fn eq(&self, other: &Rhs) -> bool { self.as_raw().eq(&other.as_raw()) } } impl PartialOrd for $ttype { fn partial_cmp(&self, other: &Rhs) -> Option { 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, From); ${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 { // 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 { 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()) } }