Skip to main content

ringkernel_core/resource/
guard.rs

1//! Resource guard for preventing system overload.
2
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::OnceLock;
5
6use super::error::{ResourceError, ResourceResult};
7use super::estimate::MemoryEstimate;
8use super::system::get_available_memory;
9use super::{DEFAULT_MAX_MEMORY_BYTES, SYSTEM_MEMORY_MARGIN};
10
11/// Resource guard for preventing system overload.
12///
13/// Tracks memory allocation and provides checks before large allocations.
14#[derive(Debug)]
15pub struct ResourceGuard {
16    /// Maximum allowed memory usage.
17    max_memory_bytes: AtomicU64,
18    /// Current tracked memory usage.
19    current_memory_bytes: AtomicU64,
20    /// Current reserved memory.
21    reserved_bytes: AtomicU64,
22    /// Whether to enforce limits (can be disabled for testing).
23    enforce_limits: AtomicBool,
24    /// Safety margin ratio (0.0-1.0).
25    safety_margin: f32,
26}
27
28impl Default for ResourceGuard {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl ResourceGuard {
35    /// Creates a new resource guard with default limits.
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            max_memory_bytes: AtomicU64::new(DEFAULT_MAX_MEMORY_BYTES),
40            current_memory_bytes: AtomicU64::new(0),
41            reserved_bytes: AtomicU64::new(0),
42            enforce_limits: AtomicBool::new(true),
43            safety_margin: 0.3, // 30% headroom
44        }
45    }
46
47    /// Creates a resource guard with a specific memory limit.
48    #[must_use]
49    pub fn with_max_memory(max_bytes: u64) -> Self {
50        Self {
51            max_memory_bytes: AtomicU64::new(max_bytes),
52            current_memory_bytes: AtomicU64::new(0),
53            reserved_bytes: AtomicU64::new(0),
54            enforce_limits: AtomicBool::new(true),
55            safety_margin: 0.3,
56        }
57    }
58
59    /// Creates a resource guard with custom safety margin.
60    #[must_use]
61    pub fn with_safety_margin(mut self, margin: f32) -> Self {
62        self.safety_margin = margin.clamp(0.0, 0.9);
63        self
64    }
65
66    /// Creates a resource guard that doesn't enforce limits (for testing).
67    #[must_use]
68    pub fn unguarded() -> Self {
69        let guard = Self::new();
70        guard.enforce_limits.store(false, Ordering::SeqCst);
71        guard
72    }
73
74    /// Sets the maximum memory limit.
75    pub fn set_max_memory(&self, max_bytes: u64) {
76        self.max_memory_bytes.store(max_bytes, Ordering::SeqCst);
77    }
78
79    /// Gets the maximum memory limit.
80    #[must_use]
81    pub fn max_memory(&self) -> u64 {
82        self.max_memory_bytes.load(Ordering::SeqCst)
83    }
84
85    /// Gets the current tracked memory usage.
86    #[must_use]
87    pub fn current_memory(&self) -> u64 {
88        self.current_memory_bytes.load(Ordering::SeqCst)
89    }
90
91    /// Gets the currently reserved memory.
92    #[must_use]
93    pub fn reserved_memory(&self) -> u64 {
94        self.reserved_bytes.load(Ordering::SeqCst)
95    }
96
97    /// Gets the effective available memory (max - current - reserved).
98    #[must_use]
99    pub fn available_memory(&self) -> u64 {
100        let max = self.max_memory();
101        let current = self.current_memory();
102        let reserved = self.reserved_memory();
103        max.saturating_sub(current).saturating_sub(reserved)
104    }
105
106    /// Checks if a given allocation can proceed.
107    #[must_use]
108    pub fn can_allocate(&self, bytes: u64) -> bool {
109        if !self.enforce_limits.load(Ordering::SeqCst) {
110            return true;
111        }
112
113        bytes <= self.available_memory()
114    }
115
116    /// Checks if an allocation can proceed, also checking system memory.
117    pub fn can_allocate_safe(&self, bytes: u64) -> ResourceResult<()> {
118        if !self.enforce_limits.load(Ordering::SeqCst) {
119            return Ok(());
120        }
121
122        // Check against our limit
123        let current = self.current_memory();
124        let reserved = self.reserved_memory();
125        let max = self.max_memory();
126        let used = current.saturating_add(reserved);
127
128        if used.saturating_add(bytes) > max {
129            return Err(ResourceError::MemoryLimitExceeded {
130                requested: bytes,
131                current: used,
132                max,
133            });
134        }
135
136        // Check system memory
137        if let Some(available) = get_available_memory() {
138            if bytes > available.saturating_sub(SYSTEM_MEMORY_MARGIN) {
139                return Err(ResourceError::InsufficientSystemMemory {
140                    requested: bytes,
141                    available,
142                    margin: SYSTEM_MEMORY_MARGIN,
143                });
144            }
145        }
146
147        Ok(())
148    }
149
150    /// Records a memory allocation.
151    pub fn record_allocation(&self, bytes: u64) {
152        self.current_memory_bytes.fetch_add(bytes, Ordering::SeqCst);
153    }
154
155    /// Records a memory deallocation.
156    pub fn record_deallocation(&self, bytes: u64) {
157        self.current_memory_bytes.fetch_sub(bytes, Ordering::SeqCst);
158    }
159
160    /// Reserves memory for a future allocation.
161    ///
162    /// Returns a guard that releases the reservation on drop.
163    pub fn reserve(&self, bytes: u64) -> ResourceResult<ReservationGuard<'_>> {
164        self.can_allocate_safe(bytes)?;
165        self.reserved_bytes.fetch_add(bytes, Ordering::SeqCst);
166        Ok(ReservationGuard {
167            guard: self,
168            bytes,
169            committed: false,
170        })
171    }
172
173    /// Validates a memory estimate before allocation.
174    pub fn validate(&self, estimate: &MemoryEstimate) -> ResourceResult<()> {
175        self.can_allocate_safe(estimate.peak_bytes)
176    }
177
178    /// Returns the maximum safe element count for a given per-element byte cost.
179    #[must_use]
180    pub fn max_safe_elements(&self, bytes_per_element: usize) -> usize {
181        if bytes_per_element == 0 {
182            return usize::MAX;
183        }
184
185        let available = self.available_memory();
186        let safe_bytes = (available as f64 * (1.0 - self.safety_margin as f64)) as u64;
187
188        (safe_bytes / bytes_per_element as u64) as usize
189    }
190
191    /// Enables or disables limit enforcement.
192    pub fn set_enforce_limits(&self, enforce: bool) {
193        self.enforce_limits.store(enforce, Ordering::SeqCst);
194    }
195
196    /// Returns whether limits are being enforced.
197    #[must_use]
198    pub fn is_enforcing(&self) -> bool {
199        self.enforce_limits.load(Ordering::SeqCst)
200    }
201}
202
203/// RAII guard for memory reservations.
204///
205/// Releases the reservation when dropped, unless committed.
206#[derive(Debug)]
207pub struct ReservationGuard<'a> {
208    guard: &'a ResourceGuard,
209    bytes: u64,
210    committed: bool,
211}
212
213impl<'a> ReservationGuard<'a> {
214    /// Returns the reserved bytes.
215    #[must_use]
216    pub fn bytes(&self) -> u64 {
217        self.bytes
218    }
219
220    /// Commits the reservation by recording it as an allocation.
221    ///
222    /// The reservation is released and replaced with an actual allocation record.
223    pub fn commit(mut self) {
224        self.guard
225            .reserved_bytes
226            .fetch_sub(self.bytes, Ordering::SeqCst);
227        self.guard.record_allocation(self.bytes);
228        self.committed = true;
229    }
230
231    /// Releases the reservation without allocating.
232    pub fn release(mut self) {
233        self.guard
234            .reserved_bytes
235            .fetch_sub(self.bytes, Ordering::SeqCst);
236        self.committed = true; // Prevent double-release in drop
237    }
238}
239
240impl<'a> Drop for ReservationGuard<'a> {
241    fn drop(&mut self) {
242        if !self.committed {
243            self.guard
244                .reserved_bytes
245                .fetch_sub(self.bytes, Ordering::SeqCst);
246        }
247    }
248}
249
250// ============================================================================
251// Global Guard
252// ============================================================================
253
254static GLOBAL_GUARD: OnceLock<ResourceGuard> = OnceLock::new();
255
256/// Gets the global resource guard.
257pub fn global_guard() -> &'static ResourceGuard {
258    GLOBAL_GUARD.get_or_init(ResourceGuard::new)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_resource_guard_new() {
267        let guard = ResourceGuard::new();
268        assert_eq!(guard.current_memory(), 0);
269        assert_eq!(guard.max_memory(), DEFAULT_MAX_MEMORY_BYTES);
270    }
271
272    #[test]
273    fn test_resource_guard_allocation() {
274        let guard = ResourceGuard::with_max_memory(1_000_000);
275
276        // Should allow small allocation
277        assert!(guard.can_allocate(100_000));
278
279        // Should deny large allocation
280        assert!(!guard.can_allocate(2_000_000));
281
282        // After recording, should update current
283        guard.record_allocation(500_000);
284        assert_eq!(guard.current_memory(), 500_000);
285
286        // Now should deny medium allocation
287        assert!(!guard.can_allocate(600_000));
288        assert!(guard.can_allocate(400_000));
289    }
290
291    #[test]
292    fn test_resource_guard_deallocation() {
293        let guard = ResourceGuard::with_max_memory(1_000_000);
294
295        guard.record_allocation(500_000);
296        assert_eq!(guard.current_memory(), 500_000);
297
298        guard.record_deallocation(200_000);
299        assert_eq!(guard.current_memory(), 300_000);
300    }
301
302    #[test]
303    fn test_reservation_guard() {
304        let guard = ResourceGuard::with_max_memory(1_000_000);
305
306        {
307            let reservation = guard.reserve(500_000).unwrap();
308            assert_eq!(guard.reserved_memory(), 500_000);
309            assert_eq!(reservation.bytes(), 500_000);
310
311            // Can't allocate more than remaining
312            assert!(!guard.can_allocate(600_000));
313        } // Reservation released on drop
314
315        assert_eq!(guard.reserved_memory(), 0);
316        assert!(guard.can_allocate(600_000));
317    }
318
319    #[test]
320    fn test_reservation_commit() {
321        let guard = ResourceGuard::with_max_memory(1_000_000);
322
323        let reservation = guard.reserve(500_000).unwrap();
324        reservation.commit();
325
326        assert_eq!(guard.reserved_memory(), 0);
327        assert_eq!(guard.current_memory(), 500_000);
328    }
329
330    #[test]
331    fn test_reservation_release() {
332        let guard = ResourceGuard::with_max_memory(1_000_000);
333
334        let reservation = guard.reserve(500_000).unwrap();
335        reservation.release();
336
337        assert_eq!(guard.reserved_memory(), 0);
338        assert_eq!(guard.current_memory(), 0);
339    }
340
341    #[test]
342    fn test_max_safe_elements() {
343        let guard = ResourceGuard::with_max_memory(1_000_000);
344
345        // With 30% safety margin, can use ~70% = ~700_000 bytes
346        // At 100 bytes per element, that's ~7000 elements
347        // Allow +/- 1 for floating point rounding
348        let max_elements = guard.max_safe_elements(100);
349        assert!(
350            (6999..=7001).contains(&max_elements),
351            "max_elements {} not in range [6999, 7001]",
352            max_elements
353        );
354    }
355
356    #[test]
357    fn test_unguarded() {
358        let guard = ResourceGuard::unguarded();
359
360        // Should always allow allocation
361        assert!(guard.can_allocate(u64::MAX / 2));
362    }
363
364    #[test]
365    fn test_global_guard() {
366        let guard = global_guard();
367        assert_eq!(guard.current_memory(), 0);
368    }
369}