Skip to main content

yo_reactor/
budget.rs

1//! What the maintenance slice is allowed to spend.
2//!
3//! `04` section 6 asks for a fixed instruction budget rather than a wall clock
4//! budget, and the difference matters. A wall clock budget reads a timer,
5//! which is a syscall or at best a serialising instruction, and it gives a
6//! different amount of work on a busy machine than on an idle one, so the tail
7//! latency it produces is not reproducible. A unit budget is a subtraction, it
8//! is the same on every machine, and a run that overshoots does so by one item
9//! rather than by however long that item took.
10//!
11//! A unit is whatever the caller says it is. The engine picks a cost per item
12//! it does, keeps the scale consistent between the things it does, and the
13//! budget only has to be monotone: more units means more work.
14
15/// Units a maintenance slice gets by default, per turn of the loop.
16///
17/// Sized so that the slice is a small fraction of a full batch of commands
18/// rather than a competitor to it. Expiry sampling is the usual spender and it
19/// walks twenty keys at a time, so this is tens of samples in the worst case
20/// and nothing at all in the common one, where there is no work waiting.
21pub const MAINTENANCE_UNITS: u32 = 4096;
22
23/// A slice's remaining allowance.
24///
25/// The contract is one call: [`Budget::spend`] returns false when the caller
26/// should stop. It is not an error and it is not something to report. A
27/// maintenance pass that runs out of budget has done part of its work and will
28/// be back on the next turn, which is a hundred nanoseconds away.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Budget {
31    left: u32,
32    spent: u32,
33}
34
35impl Budget {
36    /// A budget of `units`.
37    #[must_use]
38    pub const fn new(units: u32) -> Budget {
39        Budget {
40            left: units,
41            spent: 0,
42        }
43    }
44
45    /// A budget of [`MAINTENANCE_UNITS`].
46    #[must_use]
47    pub const fn standard() -> Budget {
48        Budget::new(MAINTENANCE_UNITS)
49    }
50
51    /// A budget of nothing, which is what a turn hands to an engine that has
52    /// asked not to be given a slice.
53    #[must_use]
54    pub const fn none() -> Budget {
55        Budget::new(0)
56    }
57
58    /// Charge `units` and say whether there is anything left to do after it.
59    ///
60    /// The charge always goes through, even when it takes the budget past the
61    /// end. Refusing it would mean the caller has to ask before every item and
62    /// then do the item anyway, which is two branches for the same answer.
63    #[inline]
64    pub const fn spend(&mut self, units: u32) -> bool {
65        self.spent = self.spent.saturating_add(units);
66        self.left = self.left.saturating_sub(units);
67        self.left > 0
68    }
69
70    /// What is left.
71    #[must_use]
72    #[inline]
73    pub const fn left(&self) -> u32 {
74        self.left
75    }
76
77    /// What has gone, which is what a counter reports rather than what the
78    /// slice decides on.
79    #[must_use]
80    #[inline]
81    pub const fn spent(&self) -> u32 {
82        self.spent
83    }
84
85    /// Whether there is any allowance at all.
86    #[must_use]
87    #[inline]
88    pub const fn is_spent(&self) -> bool {
89        self.left == 0
90    }
91}
92
93impl Default for Budget {
94    fn default() -> Budget {
95        Budget::standard()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn a_budget_runs_out_and_says_so() {
105        let mut b = Budget::new(10);
106        assert!(b.spend(4));
107        assert!(b.spend(5));
108        assert!(!b.spend(1), "the tenth unit is the last one");
109        assert!(b.is_spent());
110        assert_eq!(b.spent(), 10);
111    }
112
113    #[test]
114    fn overspending_is_allowed_and_recorded() {
115        let mut b = Budget::new(10);
116        assert!(
117            !b.spend(1000),
118            "one item can cost more than the whole slice"
119        );
120        assert_eq!(b.left(), 0);
121        assert_eq!(b.spent(), 1000, "what it cost, not what it was allowed");
122    }
123
124    #[test]
125    fn an_empty_budget_stops_before_the_first_item() {
126        let mut b = Budget::none();
127        assert!(b.is_spent());
128        assert!(!b.spend(1));
129    }
130
131    #[test]
132    fn spending_cannot_wrap() {
133        let mut b = Budget::new(u32::MAX);
134        assert!(b.spend(u32::MAX - 1));
135        assert!(!b.spend(u32::MAX));
136        assert_eq!(b.left(), 0);
137        assert_eq!(b.spent(), u32::MAX);
138    }
139}