memory_accounting/
grant.rs

1use ordered_float::NotNan;
2use snafu::Snafu;
3
4// We limit grants to a size of 2^53 (~9PB) because at numbers bigger than that, we end up with floating point loss when
5// we do scaling calculations. Since we are not going to support grants that large -- and if we ever have to, well,
6// then, I'll eat my synpatic implant or whatever makes sense to eat 40 years from now -- we just limit them like this
7// for now.
8const MAX_GRANT_BYTES: usize = 2usize.pow(f64::MANTISSA_DIGITS);
9
10#[derive(Debug, Snafu)]
11pub enum GrantError {
12    #[snafu(display("Slop factor must be between 0.0 and 1.0 inclusive."))]
13    InvalidSlopFactor,
14
15    #[snafu(display("Initial limit must be less than or equal to 9PiB (2^53 bytes)."))]
16    InitialLimitTooHigh,
17}
18
19/// A memory grant.
20///
21/// Grants define a number of bytes which have been granted for use, with an accompanying "slop factor."
22///
23/// ## Slop factor
24///
25/// There can be numerous sources of "slop" in terms of memory allocations, and limiting the memory usage of a process.
26/// Not all components can effectively track their true firm/hard limit, memory allocators have fragmentation that may
27/// be reduced over time but never fully eliminated, and so on.
28///
29/// In order to protect against this issue, we utilize a slop factor when calculating the effective limits that we
30/// should verify memory bounds again. For example, if we seek to use no more than 64MB of memory from the OS
31/// perspective (RSS), then intuitively we know that we might only be able to allocate 55-60MB of memory before
32/// allocator fragmentation causes us to reach 64MB RSS.
33///
34/// By specifying a slop factor, we can provide ourselves breathing room to ensure that we don't try to allocate every
35/// last byte of the given global limit, inevitably leading to _exceeding_ that limit and potentially causing
36/// out-of-memory crashes, etc.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct MemoryGrant {
39    initial_limit_bytes: usize,
40    slop_factor: NotNan<f64>,
41    effective_limit_bytes: usize,
42}
43
44impl MemoryGrant {
45    /// Creates a new memory grant based on the given effective limit.
46    ///
47    /// This grant will have a slop factor of 0.0 to indicate that the effective limit is already inclusive of any
48    /// necessary slop factor.
49    ///
50    /// If the effective limit is greater than 9007199254740992 bytes (2^53 bytes, or roughly 9 petabytes), then `None`
51    /// is returned. This is a hardcoded limit.
52    pub fn effective(effective_limit_bytes: usize) -> Result<Self, GrantError> {
53        Self::with_slop_factor(effective_limit_bytes, 0.0)
54    }
55
56    /// Creates a new memory grant based on the given initial limit and slop factor.
57    ///
58    /// The slop factor accounts for the percentage of the initial limit that can effectively be used. For example, a
59    /// slop factor of 0.1 would indicate that only 90% of the initial limit should be used, and a slop factor of 0.25
60    /// would indicate that only 75% of the initial limit should be used, and so on.
61    ///
62    /// If the slop factor is not valid (must be 0.0 < slop_factor <= 1.0), then `None` is returned.  If the effective
63    /// limit is greater than 9007199254740992 bytes (2^53 bytes, or roughly 9 petabytes), then `None` is returned. This
64    /// is a hardcoded limit.
65    pub fn with_slop_factor(initial_limit_bytes: usize, slop_factor: f64) -> Result<Self, GrantError> {
66        let slop_factor = if !(0.0..1.0).contains(&slop_factor) {
67            return Err(GrantError::InvalidSlopFactor);
68        } else {
69            NotNan::new(slop_factor).map_err(|_| GrantError::InvalidSlopFactor)?
70        };
71
72        if initial_limit_bytes > MAX_GRANT_BYTES {
73            return Err(GrantError::InitialLimitTooHigh);
74        }
75
76        let effective_limit_bytes = (initial_limit_bytes as f64 * (1.0 - slop_factor.into_inner())) as usize;
77        Ok(Self {
78            initial_limit_bytes,
79            slop_factor,
80            effective_limit_bytes,
81        })
82    }
83
84    /// Initial number of bytes granted.
85    ///
86    /// This value is purely informational, and should not be used to calculating the memory available for use. For that
87    /// value, see [`effective_limit_bytes`][Self::effective_limit_bytes].
88    pub fn initial_limit_bytes(&self) -> usize {
89        self.initial_limit_bytes
90    }
91
92    /// The slop factor for the initial limit.
93    ///
94    /// This value is purely informational.
95    pub fn slop_factor(&self) -> f64 {
96        self.slop_factor.into_inner()
97    }
98
99    /// Effective number of bytes granted.
100    ///
101    /// This is the value which should be followed for memory usage purposes, as it accounts for the configured slop
102    /// factor.
103    pub fn effective_limit_bytes(&self) -> usize {
104        self.effective_limit_bytes
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::MemoryGrant;
111
112    #[test]
113    fn effective() {
114        assert!(MemoryGrant::effective(1).is_ok());
115        assert!(MemoryGrant::effective(2usize.pow(f64::MANTISSA_DIGITS)).is_ok());
116        assert!(MemoryGrant::effective(2usize.pow(f64::MANTISSA_DIGITS) + 1).is_err());
117    }
118
119    #[test]
120    fn slop_factor() {
121        assert!(MemoryGrant::with_slop_factor(1, 0.1).is_ok());
122        assert!(MemoryGrant::with_slop_factor(1, 0.9).is_ok());
123        assert!(MemoryGrant::with_slop_factor(1, f64::NAN).is_err());
124        assert!(MemoryGrant::with_slop_factor(1, -0.1).is_err());
125        assert!(MemoryGrant::with_slop_factor(1, 1.001).is_err());
126        assert!(MemoryGrant::with_slop_factor(2usize.pow(f64::MANTISSA_DIGITS), 0.25).is_ok());
127        assert!(MemoryGrant::with_slop_factor(2usize.pow(f64::MANTISSA_DIGITS) + 1, 0.25).is_err());
128    }
129}