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}