aegis_resource/
limiter.rs

1//! Memory resource limiter implementation.
2//!
3//! This module provides the `AegisResourceLimiter` which implements Wasmtime's
4//! `ResourceLimiter` trait to enforce memory and table size limits.
5
6use std::sync::atomic::{AtomicUsize, Ordering};
7
8use parking_lot::Mutex;
9use tracing::{debug, warn};
10
11/// Callback type for memory growth events.
12pub type MemoryGrowthCallback = Box<dyn Fn(MemoryGrowthEvent) + Send + Sync>;
13
14/// Event emitted when memory grows.
15#[derive(Debug, Clone)]
16pub struct MemoryGrowthEvent {
17    /// Previous memory size in bytes.
18    pub from_bytes: usize,
19    /// New memory size in bytes.
20    pub to_bytes: usize,
21    /// Maximum allowed memory in bytes.
22    pub max_bytes: usize,
23}
24
25/// Configuration for the resource limiter.
26#[derive(Debug, Clone)]
27pub struct LimiterConfig {
28    /// Maximum memory in bytes.
29    pub max_memory_bytes: usize,
30    /// Maximum table elements.
31    pub max_table_elements: u32,
32    /// Maximum number of memory instances.
33    pub max_memories: u32,
34    /// Maximum number of tables.
35    pub max_tables: u32,
36}
37
38impl Default for LimiterConfig {
39    fn default() -> Self {
40        Self {
41            max_memory_bytes: 64 * 1024 * 1024, // 64MB
42            max_table_elements: 10_000,
43            max_memories: 1,
44            max_tables: 10,
45        }
46    }
47}
48
49impl LimiterConfig {
50    /// Create a new limiter configuration.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Set the maximum memory.
56    pub fn with_max_memory(mut self, bytes: usize) -> Self {
57        self.max_memory_bytes = bytes;
58        self
59    }
60
61    /// Set the maximum table elements.
62    pub fn with_max_table_elements(mut self, elements: u32) -> Self {
63        self.max_table_elements = elements;
64        self
65    }
66}
67
68/// Resource limiter that enforces memory and table limits.
69///
70/// This struct implements tracking of memory usage and can be used
71/// to monitor resource consumption during WASM execution.
72pub struct AegisResourceLimiter {
73    /// Configuration.
74    config: LimiterConfig,
75    /// Current total memory usage in bytes.
76    current_memory: AtomicUsize,
77    /// Peak memory usage in bytes.
78    peak_memory: AtomicUsize,
79    /// Number of memory allocations.
80    allocation_count: AtomicUsize,
81    /// Optional callback for memory growth events.
82    on_memory_grow: Mutex<Option<MemoryGrowthCallback>>,
83}
84
85impl AegisResourceLimiter {
86    /// Create a new resource limiter with the given configuration.
87    pub fn new(config: LimiterConfig) -> Self {
88        Self {
89            config,
90            current_memory: AtomicUsize::new(0),
91            peak_memory: AtomicUsize::new(0),
92            allocation_count: AtomicUsize::new(0),
93            on_memory_grow: Mutex::new(None),
94        }
95    }
96
97    /// Create a resource limiter with default configuration.
98    pub fn with_defaults() -> Self {
99        Self::new(LimiterConfig::default())
100    }
101
102    /// Set the memory growth callback.
103    pub fn set_memory_growth_callback(&self, callback: MemoryGrowthCallback) {
104        *self.on_memory_grow.lock() = Some(callback);
105    }
106
107    /// Get the current memory usage in bytes.
108    pub fn current_memory(&self) -> usize {
109        self.current_memory.load(Ordering::Relaxed)
110    }
111
112    /// Get the peak memory usage in bytes.
113    pub fn peak_memory(&self) -> usize {
114        self.peak_memory.load(Ordering::Relaxed)
115    }
116
117    /// Get the number of memory allocations.
118    pub fn allocation_count(&self) -> usize {
119        self.allocation_count.load(Ordering::Relaxed)
120    }
121
122    /// Get the remaining memory capacity in bytes.
123    pub fn remaining_memory(&self) -> usize {
124        self.config
125            .max_memory_bytes
126            .saturating_sub(self.current_memory())
127    }
128
129    /// Get the maximum memory limit in bytes.
130    pub fn max_memory(&self) -> usize {
131        self.config.max_memory_bytes
132    }
133
134    /// Check if memory growth is allowed.
135    ///
136    /// Returns `true` if the growth is permitted, `false` otherwise.
137    pub fn check_memory_growth(&self, current: usize, desired: usize) -> bool {
138        if desired > self.config.max_memory_bytes {
139            warn!(
140                current_bytes = current,
141                desired_bytes = desired,
142                max_bytes = self.config.max_memory_bytes,
143                "Memory growth denied: exceeds limit"
144            );
145            return false;
146        }
147
148        // Update tracking
149        self.current_memory.store(desired, Ordering::Relaxed);
150        self.allocation_count.fetch_add(1, Ordering::Relaxed);
151
152        // Update peak if necessary
153        let mut peak = self.peak_memory.load(Ordering::Relaxed);
154        while desired > peak {
155            match self.peak_memory.compare_exchange_weak(
156                peak,
157                desired,
158                Ordering::Relaxed,
159                Ordering::Relaxed,
160            ) {
161                Ok(_) => break,
162                Err(current_peak) => peak = current_peak,
163            }
164        }
165
166        // Emit callback if set
167        if let Some(callback) = self.on_memory_grow.lock().as_ref() {
168            callback(MemoryGrowthEvent {
169                from_bytes: current,
170                to_bytes: desired,
171                max_bytes: self.config.max_memory_bytes,
172            });
173        }
174
175        debug!(
176            from_bytes = current,
177            to_bytes = desired,
178            peak_bytes = self.peak_memory(),
179            "Memory growth permitted"
180        );
181
182        true
183    }
184
185    /// Check if table growth is allowed.
186    pub fn check_table_growth(&self, current: u32, desired: u32) -> bool {
187        if desired > self.config.max_table_elements {
188            warn!(
189                current_elements = current,
190                desired_elements = desired,
191                max_elements = self.config.max_table_elements,
192                "Table growth denied: exceeds limit"
193            );
194            return false;
195        }
196
197        debug!(
198            from_elements = current,
199            to_elements = desired,
200            "Table growth permitted"
201        );
202
203        true
204    }
205
206    /// Reset the limiter statistics.
207    pub fn reset(&self) {
208        self.current_memory.store(0, Ordering::Relaxed);
209        self.peak_memory.store(0, Ordering::Relaxed);
210        self.allocation_count.store(0, Ordering::Relaxed);
211    }
212
213    /// Get a snapshot of the current statistics.
214    pub fn stats(&self) -> LimiterStats {
215        LimiterStats {
216            current_memory: self.current_memory(),
217            peak_memory: self.peak_memory(),
218            allocation_count: self.allocation_count(),
219            max_memory: self.config.max_memory_bytes,
220        }
221    }
222}
223
224/// Statistics snapshot from a resource limiter.
225#[derive(Debug, Clone)]
226pub struct LimiterStats {
227    /// Current memory usage in bytes.
228    pub current_memory: usize,
229    /// Peak memory usage in bytes.
230    pub peak_memory: usize,
231    /// Number of memory allocations.
232    pub allocation_count: usize,
233    /// Maximum memory limit in bytes.
234    pub max_memory: usize,
235}
236
237impl LimiterStats {
238    /// Calculate memory utilization as a percentage.
239    pub fn utilization_percent(&self) -> f64 {
240        if self.max_memory == 0 {
241            0.0
242        } else {
243            (self.peak_memory as f64 / self.max_memory as f64) * 100.0
244        }
245    }
246}
247
248impl std::fmt::Debug for AegisResourceLimiter {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("AegisResourceLimiter")
251            .field("config", &self.config)
252            .field("current_memory", &self.current_memory())
253            .field("peak_memory", &self.peak_memory())
254            .field("allocation_count", &self.allocation_count())
255            .finish()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use std::sync::Arc;
263
264    #[test]
265    fn test_limiter_creation() {
266        let limiter = AegisResourceLimiter::new(LimiterConfig::default());
267        assert_eq!(limiter.current_memory(), 0);
268        assert_eq!(limiter.peak_memory(), 0);
269    }
270
271    #[test]
272    fn test_memory_growth_allowed() {
273        let config = LimiterConfig::default().with_max_memory(1024 * 1024);
274        let limiter = AegisResourceLimiter::new(config);
275
276        assert!(limiter.check_memory_growth(0, 512 * 1024));
277        assert_eq!(limiter.current_memory(), 512 * 1024);
278    }
279
280    #[test]
281    fn test_memory_growth_denied() {
282        let config = LimiterConfig::default().with_max_memory(1024 * 1024);
283        let limiter = AegisResourceLimiter::new(config);
284
285        assert!(!limiter.check_memory_growth(0, 2 * 1024 * 1024));
286    }
287
288    #[test]
289    fn test_peak_memory_tracking() {
290        let config = LimiterConfig::default().with_max_memory(10 * 1024 * 1024);
291        let limiter = AegisResourceLimiter::new(config);
292
293        limiter.check_memory_growth(0, 1024);
294        limiter.check_memory_growth(1024, 2048);
295        limiter.check_memory_growth(2048, 1024); // Shrink
296
297        assert_eq!(limiter.peak_memory(), 2048);
298        assert_eq!(limiter.current_memory(), 1024);
299    }
300
301    #[test]
302    fn test_memory_growth_callback() {
303        use std::sync::atomic::AtomicBool;
304
305        let callback_called = Arc::new(AtomicBool::new(false));
306        let callback_called_clone = Arc::clone(&callback_called);
307
308        let limiter = AegisResourceLimiter::with_defaults();
309        limiter.set_memory_growth_callback(Box::new(move |_event| {
310            callback_called_clone.store(true, Ordering::SeqCst);
311        }));
312
313        limiter.check_memory_growth(0, 1024);
314        assert!(callback_called.load(Ordering::SeqCst));
315    }
316
317    #[test]
318    fn test_table_growth() {
319        let config = LimiterConfig::default().with_max_table_elements(1000);
320        let limiter = AegisResourceLimiter::new(config);
321
322        assert!(limiter.check_table_growth(0, 500));
323        assert!(!limiter.check_table_growth(500, 1500));
324    }
325
326    #[test]
327    fn test_stats() {
328        let config = LimiterConfig::default().with_max_memory(1024);
329        let limiter = AegisResourceLimiter::new(config);
330
331        limiter.check_memory_growth(0, 512);
332
333        let stats = limiter.stats();
334        assert_eq!(stats.current_memory, 512);
335        assert_eq!(stats.peak_memory, 512);
336        assert_eq!(stats.max_memory, 1024);
337        assert!((stats.utilization_percent() - 50.0).abs() < 0.01);
338    }
339}