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}