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

            
15
use super::*;
16

            
17
define_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
182712
        fn as_raw(&self) -> Qty {
36
            self.raw
37
        }
38
    }
39

            
40
    impl<Rhs: BookkeepableQty> PartialEq<Rhs> for $ttype {
41
4
        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
43584
        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
56308
        fn from_raw(q: Qty) -> Self {
55
            $ttype {
56
                raw: q,
57
              ${if BOMB {
58
                bomb: DropBombCondition::new_armed(),
59
              }}
60
            }
61
        }
62
124792
        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
83740
        fn drop(&mut self) {
73
83740
            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).
91
pub(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.
117
trait 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

            
126
impl BookkeepableQty for Qty {
127
    const ZERO: Qty = Qty(0);
128

            
129
43588
    fn as_raw(&self) -> Qty {
130
43588
        *self
131
43588
    }
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
144
pub(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}")]
157
pub(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]
178
pub(super) struct ClaimedQty {
179
    /// See [`BookkeptQty`]
180
    raw: Qty,
181

            
182
    /// See [`BookkeptQty`]
183
    bomb: DropBombCondition,
184
}
185

            
186
impl 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
23332
    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
23332
        let new_self = self.raw.checked_add(*want)?;
196
23332
        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
23332
        }
203
23332
        let new_p_used = p_used.raw.checked_add(*want)?;
204
        // commit
205
23332
        self.raw = Qty(new_self);
206
23332
        p_used.raw = Qty(new_p_used);
207
23332
        Some(ClaimedQty::from_raw(want))
208
23332
    }
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
19968
    pub(super) fn release(&mut self, p_used: &mut ParticipQty, have: ClaimedQty) {
214
19968
        let have = have.into_raw();
215
19968
        *p_used.raw = p_used.raw.saturating_sub(*have);
216
19968

            
217
19968
        if self.raw != Qty::MAX {
218
19968
            // Don't unpoison
219
19968
            *self.raw = self.raw.saturating_sub(*have);
220
19968
        }
221
19968
    }
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

            
229
impl ClaimedQty {
230
    /// Split a `ClaimedQty` into two `ClaimedQty`s
231
26816
    pub(super) fn split_off(&mut self, want: Qty) -> Option<ClaimedQty> {
232
26816
        let new_self = self.raw.checked_sub(*want)?;
233
        // commit
234
12712
        *self.raw = new_self;
235
12712
        Some(ClaimedQty::from_raw(want))
236
26816
    }
237

            
238
    /// Merge two `ClaimedQty`s
239
    ///
240
    /// (Handles overflow by saturating; returning an error is not going to be useful.)
241
29156
    pub(super) fn merge_into(&mut self, have: ClaimedQty) {
242
29156
        let have = have.into_raw();
243
29156
        *self.raw = self.raw.saturating_add(*have);
244
29156
    }
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
20404
    pub(super) fn claim_return_to_participant(self) -> crate::Result<()> {
253
20404
        let _: Qty = self.into_raw();
254
20404
        Ok(())
255
20404
    }
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
20096
    pub(super) fn release_got_from_participant(got: Qty) -> Self {
265
20096
        ClaimedQty::from_raw(got)
266
20096
    }
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
55264
    pub(super) fn dispose_participant_destroyed(mut self) {
279
55264
        let _: Qty = mem::take(&mut self).into_raw();
280
55264
    }
281
}
282

            
283
impl 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
168
    pub(super) fn for_participant_teardown(&self) -> ClaimedQty {
304
168
        // We imagine that the Participant said it was releasing everything
305
168
        ClaimedQty::from_raw(self.as_raw())
306
168
    }
307
}