Skip to main content

dev_chaos/
memory_pressure.rs

1//! Memory-pressure helpers for chaos testing.
2//!
3//! These helpers allocate (and hold) bytes to simulate a system
4//! under memory pressure. They DO NOT trigger OOM; the OS or Rust
5//! allocator will respond to allocation failure normally. They DO
6//! reliably consume the requested amount of process memory while
7//! the returned guard is live.
8//!
9//! ## Use cases
10//!
11//! - Verify that retry loops handle `Err` from allocation-prone paths.
12//! - Exercise code that consults available memory before issuing
13//!   large operations (e.g. buffered I/O, image decoding).
14//! - Pair with [`crate::FailureSchedule`] to simulate "memory
15//!   pressure → operation fails" scenarios deterministically.
16//!
17//! ## Limits
18//!
19//! - This is *user-space* pressure. It cannot simulate kernel-level
20//!   exhaustion (NUMA, cgroups, swap thrashing) — for that you need
21//!   OS-level fault injection, which is out of scope for `dev-chaos`.
22//! - The allocation is held in `Vec<u8>` form; the OS may compress,
23//!   page out, or otherwise account for it differently than
24//!   "wasted" memory in real workloads.
25//! - Released on drop, like every other RAII guard in the suite.
26
27/// A memory-pressure guard that holds `size_bytes` of allocated memory
28/// for its entire lifetime.
29///
30/// Drop the guard to release the memory.
31///
32/// # Example
33///
34/// ```
35/// use dev_chaos::memory_pressure::MemoryPressure;
36///
37/// {
38///     let _hold = MemoryPressure::allocate(1_024 * 1_024); // 1 MiB
39///     // ... run code under memory pressure ...
40/// } // memory released here
41/// ```
42pub struct MemoryPressure {
43    _bytes: Vec<u8>,
44    /// How many bytes were requested. Stored for diagnostics.
45    size_bytes: usize,
46}
47
48impl MemoryPressure {
49    /// Allocate and hold `size_bytes` of zeroed memory.
50    ///
51    /// Always succeeds for sizes the allocator can satisfy. Returns
52    /// the guard; drop it to release the memory.
53    ///
54    /// For testing OOM / allocation-failure paths specifically, see
55    /// [`MemoryPressure::try_allocate`].
56    pub fn allocate(size_bytes: usize) -> Self {
57        let bytes = vec![0u8; size_bytes];
58        Self {
59            _bytes: bytes,
60            size_bytes,
61        }
62    }
63
64    /// Attempt to allocate `size_bytes`, returning the error path
65    /// rather than panicking on allocator failure.
66    ///
67    /// This catches `try_reserve_exact` errors. For genuinely OOM
68    /// scenarios on most platforms the kernel may kill the process
69    /// before this returns; honest behavior is platform-specific.
70    pub fn try_allocate(size_bytes: usize) -> Result<Self, std::collections::TryReserveError> {
71        let mut bytes: Vec<u8> = Vec::new();
72        bytes.try_reserve_exact(size_bytes)?;
73        bytes.resize(size_bytes, 0);
74        Ok(Self {
75            _bytes: bytes,
76            size_bytes,
77        })
78    }
79
80    /// The number of bytes this guard is holding.
81    pub fn size_bytes(&self) -> usize {
82        self.size_bytes
83    }
84
85    /// Convenience: allocate `kib` KiB.
86    pub fn allocate_kib(kib: usize) -> Self {
87        Self::allocate(kib.saturating_mul(1024))
88    }
89
90    /// Convenience: allocate `mib` MiB.
91    pub fn allocate_mib(mib: usize) -> Self {
92        Self::allocate(mib.saturating_mul(1024 * 1024))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn allocate_holds_requested_bytes() {
102        let g = MemoryPressure::allocate(64 * 1024);
103        assert_eq!(g.size_bytes(), 64 * 1024);
104    }
105
106    #[test]
107    fn try_allocate_succeeds_for_small_allocations() {
108        let g = MemoryPressure::try_allocate(4 * 1024).unwrap();
109        assert_eq!(g.size_bytes(), 4 * 1024);
110    }
111
112    #[test]
113    fn allocate_kib_and_mib_helpers() {
114        let g_kib = MemoryPressure::allocate_kib(8);
115        assert_eq!(g_kib.size_bytes(), 8 * 1024);
116        let g_mib = MemoryPressure::allocate_mib(1);
117        assert_eq!(g_mib.size_bytes(), 1024 * 1024);
118    }
119
120    #[test]
121    fn dropping_guard_releases_memory() {
122        // We can't reliably observe RSS in a unit test, but we can
123        // confirm the guard is droppable and yields no panic.
124        {
125            let _g = MemoryPressure::allocate(16 * 1024);
126        }
127        // No panic, no leak detectable here.
128    }
129
130    #[test]
131    fn try_allocate_huge_returns_err() {
132        // Request something so large the OS will refuse to reserve.
133        // usize::MAX always fails.
134        let r = MemoryPressure::try_allocate(usize::MAX);
135        assert!(r.is_err());
136    }
137}