tor_memquota/mtracker/
bookkeeping.rs

1//! Quantity bookkeeping
2//!
3//! Newtypes which wrap up a `Qty` (an amount of memory),
4//! and which assure proper accounting.
5//!
6//! Methods are provided for the specific transactions which are correct,
7//! in the accounting scheme in [`tracker`](super).
8//! So these types embody the data structure (fields and invariants) from `tracker`.
9//!
10//! # Panics
11//!
12//! In tests, these types panic if they are dropped when nonzero,
13//! if that's against the rules.
14
15use super::*;
16
17define_derive_deftly! {
18    /// Implement [`BookkeptQty`] and its supertraits
19    ///
20    /// By default, dropping when nonzero is forbidden,
21    /// and you must have a field `bomb: `[`DropBombCondition`].
22    /// `#[deftly(allow_nonzero_drop)]` suppresses this.
23    BookkeptQty:
24
25    ${defcond BOMB not(tmeta(allow_nonzero_drop))}
26
27    impl BookkeepableQty for $ttype {
28        const ZERO: $ttype = $ttype {
29            raw: Qty(0),
30          ${if BOMB {
31            bomb: DropBombCondition::new_armed(),
32          }}
33        };
34
35        fn as_raw(&self) -> Qty {
36            self.raw
37        }
38    }
39
40    impl<Rhs: BookkeepableQty> PartialEq<Rhs> for $ttype {
41        fn eq(&self, other: &Rhs) -> bool {
42            self.as_raw().eq(&other.as_raw())
43        }
44    }
45    impl<Rhs: BookkeepableQty> PartialOrd<Rhs> for $ttype {
46        fn partial_cmp(&self, other: &Rhs) -> Option<Ordering> {
47            self.as_raw().partial_cmp(&other.as_raw())
48        }
49    }
50
51    impl DefaultExtTake for $ttype {}
52
53    impl BookkeptQty for $ttype {
54        fn from_raw(q: Qty) -> Self {
55            $ttype {
56                raw: q,
57              ${if BOMB {
58                bomb: DropBombCondition::new_armed(),
59              }}
60            }
61        }
62        fn into_raw(mut self) -> Qty {
63            mem::replace(&mut self.raw, Qty(0))
64        }
65    }
66
67    assert_not_impl_any!($ttype: Clone, Into<Qty>, From<Qty>);
68
69  ${if BOMB {
70    #[cfg(test)]
71    impl Drop for $ttype {
72        fn drop(&mut self) {
73            drop_bomb_disarm_assert!(self.bomb, self.raw == Qty(0));
74        }
75    }
76  }}
77}
78
79/// Memory quantities that can work with bookkept quantities
80///
81/// This trait doesn't imply any invariants;
82/// it merely provides read-only access to the underlying value,
83/// and ways to make a zero.
84///
85/// Used by the derived `PartialEq` and `PartialOrd` impls on bookkept quantities.
86///
87/// Implemented by hand for `Qty`.
88///
89/// Implemented for bookkept types, along with `BookkeptQty`, by
90/// [`#[derive_deftly(BookKept)]`](derive_deftly_template_BookkeptQty).
91pub(super) trait BookkeepableQty: Default {
92    /// Zero (default value)
93    const ZERO: Self;
94
95    /// Inspect as a raw untracked Qty
96    fn as_raw(&self) -> Qty;
97}
98
99/// Bookkept memory quantities
100///
101/// Each bookkept quantity implements this trait,
102/// and has a single field `raw` of type `Qty`.
103///
104/// Should be Implemented by
105/// [`#[derive_deftly(BookKept)]`](derive_deftly_template_BookkeptQty)
106/// and for raw `Qty`.
107///
108/// # CORRECTNESS
109///
110/// All accesses to `raw`, or calls to `from_raw` or `into_raw`,
111/// should be made from transaction functions,
112/// which modify one or more bookkept quantities together,
113/// preserving the invariants.
114///
115/// `raw` may be accessed mutably by such functions, but a bookkept quantity type
116/// should be constructed only with `from_raw` and should not be moved out of.
117trait BookkeptQty: BookkeepableQty + DefaultExtTake {
118    /// Make a new bookkept quantity from a raw untracked Qty
119    ///
120    fn from_raw(q: Qty) -> Self;
121
122    /// Unwrap into a raw untracked Qty
123    fn into_raw(self) -> Qty;
124}
125
126impl BookkeepableQty for Qty {
127    const ZERO: Qty = Qty(0);
128
129    fn as_raw(&self) -> Qty {
130        *self
131    }
132}
133
134/// Total used, [`TotalQtyNotifier`].`total_used`, found in [`State`].`total_used`.
135///
136/// Can be "poisoned", preventing further claims.
137/// (We mark it poisoned if the reclamation task crashes,
138/// since in that situation we don't want to continue to use memory, unboundedly.)
139//
140// Poisoned is indicated by We setting to `MAX`.
141#[derive(Default, Debug, Deftly, derive_more::Display)]
142#[derive_deftly(BookkeptQty)]
143#[deftly(allow_nonzero_drop)] // Dropped only when the whole tracker is dropped
144pub(super) struct TotalQty {
145    /// See [`BookkeptQty`]
146    raw: Qty,
147}
148
149/// Qty used by a participant, found in [`PRecord`].`used`.
150///
151/// The tracker data structure has one of these for each Participant.
152///
153/// This is the total amount `claim`ed, plus the caches in each `Participation`.
154#[derive(Default, Debug, Deftly, derive_more::Display)]
155#[derive_deftly(BookkeptQty)]
156#[display("{raw}")]
157pub(super) struct ParticipQty {
158    /// See [`BookkeptQty`]
159    raw: Qty,
160
161    /// See [`BookkeptQty`]
162    bomb: DropBombCondition,
163}
164
165/// "Cached" claim, on behalf of a Participant
166///
167/// Found in [`Participation`].`cache`,
168/// and accounted to the Participant (ie, included in `ParticipQty`).
169///
170/// Also used as a temporary variable in `claim()` and `release()` functions.
171/// When we return to the participant, outside the tracker, we
172/// essentially throw this away, since we don't give the caller any representation
173/// to store.  The participant is supposed to track this separately somehow.
174#[derive(Default, Debug, Deftly, derive_more::Display)]
175#[derive_deftly(BookkeptQty)]
176#[display("{raw}")]
177#[must_use]
178pub(super) struct ClaimedQty {
179    /// See [`BookkeptQty`]
180    raw: Qty,
181
182    /// See [`BookkeptQty`]
183    bomb: DropBombCondition,
184}
185
186impl TotalQty {
187    /// Claim a quantity, increasing the tracked amounts
188    ///
189    /// This module doesn't know anything about the memory quota,
190    /// so this doesn't do the quota check.
191    ///
192    /// The only caller is [`Participation::claim`].
193    pub(super) fn claim(&mut self, p_used: &mut ParticipQty, want: Qty) -> Option<ClaimedQty> {
194        // If poisoned, this add will fail (unless want is 0)
195        let new_self = self.raw.checked_add(*want)?;
196        if new_self == usize::MAX {
197            // This would poison us.  If this happens, someone has gone mad, since
198            // we can't have allocated usize::MAX in total.  We'll be reclaiming already.
199            // We don't want to poison ourselves in this situation.  Hopefully the reclaim
200            // will collapse the errant participants.
201            return None;
202        }
203        let new_p_used = p_used.raw.checked_add(*want)?;
204        // commit
205        self.raw = Qty(new_self);
206        p_used.raw = Qty(new_p_used);
207        Some(ClaimedQty::from_raw(want))
208    }
209
210    /// Release a quantity, decreasing the tracked amounts
211    ///
212    /// (Handles underflow by saturating; returning an error is not going to be useful.)
213    pub(super) fn release(&mut self, p_used: &mut ParticipQty, have: ClaimedQty) {
214        let have = have.into_raw();
215        *p_used.raw = p_used.raw.saturating_sub(*have);
216
217        if self.raw != Qty::MAX {
218            // Don't unpoison
219            *self.raw = self.raw.saturating_sub(*have);
220        }
221    }
222
223    /// Declare this poisoned, and prevent further claims
224    pub(super) fn set_poisoned(&mut self) {
225        self.raw = Qty::MAX;
226    }
227}
228
229impl ClaimedQty {
230    /// Split a `ClaimedQty` into two `ClaimedQty`s
231    pub(super) fn split_off(&mut self, want: Qty) -> Option<ClaimedQty> {
232        let new_self = self.raw.checked_sub(*want)?;
233        // commit
234        *self.raw = new_self;
235        Some(ClaimedQty::from_raw(want))
236    }
237
238    /// Merge two `ClaimedQty`s
239    ///
240    /// (Handles overflow by saturating; returning an error is not going to be useful.)
241    pub(super) fn merge_into(&mut self, have: ClaimedQty) {
242        let have = have.into_raw();
243        *self.raw = self.raw.saturating_add(*have);
244    }
245
246    /// Obtain result for the participant, after having successfully recorded the amount claimed
247    ///
248    /// # CORRECTNESS
249    ///
250    /// This must be called only on a successful return path from [`Participation::claim`].
251    #[allow(clippy::unnecessary_wraps)] // returns Result; proves it's used on success path
252    pub(super) fn claim_return_to_participant(self) -> crate::Result<()> {
253        let _: Qty = self.into_raw();
254        Ok(())
255    }
256
257    /// When the participant indicates a release, enrol the amount in our bookkeping scheme
258    ///
259    /// Handles the quantity argument to [`Participation::release`].
260    ///
261    /// # CORRECTNESS
262    ///
263    /// This must be called only on entry to [`Participation::release`].
264    pub(super) fn release_got_from_participant(got: Qty) -> Self {
265        ClaimedQty::from_raw(got)
266    }
267
268    /// Dispose of a quantity that was claimed by a now-destroyed participant
269    ///
270    /// # CORRECTNESS
271    ///
272    /// The `ParticipQty` this was claimed from must also have been destroyed.
273    ///
274    /// So,
275    /// [`ParticipQty::for_participant_teardown`] and the corresponding
276    /// [`release`](TotalQty::release)
277    /// must have been called earlier - possibly, much earlier.
278    pub(super) fn dispose_participant_destroyed(mut self) {
279        let _: Qty = mem::take(&mut self).into_raw();
280    }
281}
282
283impl ParticipQty {
284    /// Prepare to destroy the `ParticipQty` in a participant that's being destroyed
285    ///
286    /// When the records of a participant that is being torn down are being destroyed,
287    /// we must remove our records of the memory that it allocated.
288    ///
289    /// This function is for that situation.
290    /// The returned `ClaimedQty` should then be passed to `release`.
291    ///
292    /// # CORRECTNESS
293    ///
294    /// The data structure where this `ParticipQty` resides
295    /// must be torn down (after we return).
296    ///
297    /// The `ClaimedQty` must be passed to [`TotalQty::release`].
298    //
299    // We could provide this as a single transaction function, rather than requiring
300    // two calls.  But the main code doesn't have a `TotalQty`, only a `TotalQtyNotifier`,
301    // so we'd need to add an additional passthrough method to `TotalQtyNotifier`,
302    // which doesn't seem worth it given that there's only one call site for this fn.
303    pub(super) fn for_participant_teardown(&self) -> ClaimedQty {
304        // We imagine that the Participant said it was releasing everything
305        ClaimedQty::from_raw(self.as_raw())
306    }
307}