Skip to main content

oxigdal_edge/
resource.rs

1//! Resource management for constrained edge devices
2//!
3//! Monitors and manages CPU, memory, and storage resources to ensure
4//! efficient operation on resource-limited devices.
5
6use crate::error::{EdgeError, Result};
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11
12/// Resource constraints for edge devices
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ResourceConstraints {
15    /// Maximum memory usage in bytes
16    pub max_memory_bytes: usize,
17    /// Maximum CPU usage percentage (0-100)
18    pub max_cpu_percent: f64,
19    /// Maximum storage usage in bytes
20    pub max_storage_bytes: usize,
21    /// Maximum concurrent operations
22    pub max_concurrent_ops: usize,
23    /// Operation timeout in seconds
24    pub operation_timeout_secs: u64,
25}
26
27impl Default for ResourceConstraints {
28    fn default() -> Self {
29        Self {
30            max_memory_bytes: crate::MAX_MEMORY_USAGE,
31            max_cpu_percent: 80.0,
32            max_storage_bytes: 100 * 1024 * 1024, // 100 MB
33            max_concurrent_ops: 10,
34            operation_timeout_secs: 30,
35        }
36    }
37}
38
39impl ResourceConstraints {
40    /// Create constraints for minimal embedded devices
41    pub fn minimal() -> Self {
42        Self {
43            max_memory_bytes: 10 * 1024 * 1024, // 10 MB
44            max_cpu_percent: 50.0,
45            max_storage_bytes: 10 * 1024 * 1024, // 10 MB
46            max_concurrent_ops: 3,
47            operation_timeout_secs: 10,
48        }
49    }
50
51    /// Create constraints for moderate edge devices
52    pub fn moderate() -> Self {
53        Self {
54            max_memory_bytes: 50 * 1024 * 1024, // 50 MB
55            max_cpu_percent: 70.0,
56            max_storage_bytes: 100 * 1024 * 1024, // 100 MB
57            max_concurrent_ops: 10,
58            operation_timeout_secs: 30,
59        }
60    }
61
62    /// Create constraints for powerful edge devices
63    pub fn powerful() -> Self {
64        Self {
65            max_memory_bytes: 200 * 1024 * 1024, // 200 MB
66            max_cpu_percent: 90.0,
67            max_storage_bytes: 500 * 1024 * 1024, // 500 MB
68            max_concurrent_ops: 50,
69            operation_timeout_secs: 60,
70        }
71    }
72
73    /// Validate constraints
74    pub fn validate(&self) -> Result<()> {
75        if self.max_memory_bytes == 0 {
76            return Err(EdgeError::invalid_config("max_memory_bytes must be > 0"));
77        }
78        if self.max_cpu_percent <= 0.0 || self.max_cpu_percent > 100.0 {
79            return Err(EdgeError::invalid_config(
80                "max_cpu_percent must be between 0 and 100",
81            ));
82        }
83        if self.max_storage_bytes == 0 {
84            return Err(EdgeError::invalid_config("max_storage_bytes must be > 0"));
85        }
86        if self.max_concurrent_ops == 0 {
87            return Err(EdgeError::invalid_config("max_concurrent_ops must be > 0"));
88        }
89        Ok(())
90    }
91}
92
93/// Resource usage metrics
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ResourceMetrics {
96    /// Current memory usage in bytes
97    pub memory_bytes: usize,
98    /// Current CPU usage percentage
99    pub cpu_percent: f64,
100    /// Current storage usage in bytes
101    pub storage_bytes: usize,
102    /// Number of active operations
103    pub active_operations: usize,
104    /// Peak memory usage
105    pub peak_memory_bytes: usize,
106    /// Peak CPU usage
107    pub peak_cpu_percent: f64,
108    /// Total operations completed
109    pub total_operations: u64,
110    /// Failed operations count
111    pub failed_operations: u64,
112}
113
114impl Default for ResourceMetrics {
115    fn default() -> Self {
116        Self {
117            memory_bytes: 0,
118            cpu_percent: 0.0,
119            storage_bytes: 0,
120            active_operations: 0,
121            peak_memory_bytes: 0,
122            peak_cpu_percent: 0.0,
123            total_operations: 0,
124            failed_operations: 0,
125        }
126    }
127}
128
129impl ResourceMetrics {
130    /// Get memory usage percentage
131    pub fn memory_percent(&self, max_memory: usize) -> f64 {
132        if max_memory == 0 {
133            return 0.0;
134        }
135        (self.memory_bytes as f64 / max_memory as f64) * 100.0
136    }
137
138    /// Get storage usage percentage
139    pub fn storage_percent(&self, max_storage: usize) -> f64 {
140        if max_storage == 0 {
141            return 0.0;
142        }
143        (self.storage_bytes as f64 / max_storage as f64) * 100.0
144    }
145
146    /// Get success rate
147    pub fn success_rate(&self) -> f64 {
148        if self.total_operations == 0 {
149            return 100.0;
150        }
151        let successful = self.total_operations - self.failed_operations;
152        (successful as f64 / self.total_operations as f64) * 100.0
153    }
154}
155
156/// Resource manager for edge devices
157pub struct ResourceManager {
158    constraints: ResourceConstraints,
159    memory_used: Arc<AtomicUsize>,
160    storage_used: Arc<AtomicUsize>,
161    active_ops: Arc<AtomicUsize>,
162    total_ops: Arc<AtomicU64>,
163    failed_ops: Arc<AtomicU64>,
164    peak_memory: Arc<AtomicUsize>,
165    cpu_samples: Arc<RwLock<Vec<f64>>>,
166}
167
168impl ResourceManager {
169    /// Create new resource manager
170    pub fn new(constraints: ResourceConstraints) -> Result<Self> {
171        constraints.validate()?;
172
173        Ok(Self {
174            constraints,
175            memory_used: Arc::new(AtomicUsize::new(0)),
176            storage_used: Arc::new(AtomicUsize::new(0)),
177            active_ops: Arc::new(AtomicUsize::new(0)),
178            total_ops: Arc::new(AtomicU64::new(0)),
179            failed_ops: Arc::new(AtomicU64::new(0)),
180            peak_memory: Arc::new(AtomicUsize::new(0)),
181            cpu_samples: Arc::new(RwLock::new(Vec::new())),
182        })
183    }
184
185    /// Check if operation can be started
186    pub fn can_start_operation(&self) -> Result<()> {
187        // Check concurrent operations limit
188        let active = self.active_ops.load(Ordering::Relaxed);
189        if active >= self.constraints.max_concurrent_ops {
190            return Err(EdgeError::resource_constraint(format!(
191                "Maximum concurrent operations ({}) reached",
192                self.constraints.max_concurrent_ops
193            )));
194        }
195
196        // Check memory constraint
197        let memory = self.memory_used.load(Ordering::Relaxed);
198        if memory >= self.constraints.max_memory_bytes {
199            return Err(EdgeError::resource_constraint(format!(
200                "Memory limit ({} bytes) exceeded",
201                self.constraints.max_memory_bytes
202            )));
203        }
204
205        Ok(())
206    }
207
208    /// Start an operation
209    pub fn start_operation(&self) -> Result<OperationGuard> {
210        self.can_start_operation()?;
211
212        self.active_ops.fetch_add(1, Ordering::Relaxed);
213        self.total_ops.fetch_add(1, Ordering::Relaxed);
214
215        Ok(OperationGuard {
216            active_ops: Arc::clone(&self.active_ops),
217        })
218    }
219
220    /// Record operation failure
221    pub fn record_failure(&self) {
222        self.failed_ops.fetch_add(1, Ordering::Relaxed);
223    }
224
225    /// Allocate memory
226    pub fn allocate_memory(&self, bytes: usize) -> Result<MemoryGuard> {
227        let current = self.memory_used.load(Ordering::Relaxed);
228        let new_total = current.saturating_add(bytes);
229
230        if new_total > self.constraints.max_memory_bytes {
231            return Err(EdgeError::resource_constraint(format!(
232                "Memory allocation of {} bytes would exceed limit of {} bytes",
233                bytes, self.constraints.max_memory_bytes
234            )));
235        }
236
237        self.memory_used.fetch_add(bytes, Ordering::Relaxed);
238
239        // Update peak memory
240        let mut peak = self.peak_memory.load(Ordering::Relaxed);
241        while new_total > peak {
242            match self.peak_memory.compare_exchange_weak(
243                peak,
244                new_total,
245                Ordering::Relaxed,
246                Ordering::Relaxed,
247            ) {
248                Ok(_) => break,
249                Err(current) => peak = current,
250            }
251        }
252
253        Ok(MemoryGuard {
254            bytes,
255            memory_used: Arc::clone(&self.memory_used),
256        })
257    }
258
259    /// Allocate storage
260    pub fn allocate_storage(&self, bytes: usize) -> Result<StorageGuard> {
261        let current = self.storage_used.load(Ordering::Relaxed);
262        let new_total = current.saturating_add(bytes);
263
264        if new_total > self.constraints.max_storage_bytes {
265            return Err(EdgeError::resource_constraint(format!(
266                "Storage allocation of {} bytes would exceed limit of {} bytes",
267                bytes, self.constraints.max_storage_bytes
268            )));
269        }
270
271        self.storage_used.fetch_add(bytes, Ordering::Relaxed);
272
273        Ok(StorageGuard {
274            bytes,
275            storage_used: Arc::clone(&self.storage_used),
276        })
277    }
278
279    /// Record CPU sample
280    pub fn record_cpu_sample(&self, cpu_percent: f64) {
281        let mut samples = self.cpu_samples.write();
282        samples.push(cpu_percent);
283
284        // Keep only last 100 samples
285        if samples.len() > 100 {
286            samples.remove(0);
287        }
288    }
289
290    /// Get current CPU usage (averaged over recent samples)
291    pub fn current_cpu(&self) -> f64 {
292        let samples = self.cpu_samples.read();
293        if samples.is_empty() {
294            return 0.0;
295        }
296
297        let sum: f64 = samples.iter().sum();
298        sum / samples.len() as f64
299    }
300
301    /// Check if CPU is overloaded
302    pub fn is_cpu_overloaded(&self) -> bool {
303        self.current_cpu() > self.constraints.max_cpu_percent
304    }
305
306    /// Get current metrics
307    pub fn metrics(&self) -> ResourceMetrics {
308        let memory = self.memory_used.load(Ordering::Relaxed);
309        let storage = self.storage_used.load(Ordering::Relaxed);
310        let active_ops = self.active_ops.load(Ordering::Relaxed);
311        let total_ops = self.total_ops.load(Ordering::Relaxed);
312        let failed_ops = self.failed_ops.load(Ordering::Relaxed);
313        let peak_memory = self.peak_memory.load(Ordering::Relaxed);
314
315        let cpu_samples = self.cpu_samples.read();
316        let (cpu_current, cpu_peak) = if cpu_samples.is_empty() {
317            (0.0, 0.0)
318        } else {
319            let sum: f64 = cpu_samples.iter().sum();
320            let avg = sum / cpu_samples.len() as f64;
321            let peak = cpu_samples.iter().copied().fold(0.0, f64::max);
322            (avg, peak)
323        };
324
325        ResourceMetrics {
326            memory_bytes: memory,
327            cpu_percent: cpu_current,
328            storage_bytes: storage,
329            active_operations: active_ops,
330            peak_memory_bytes: peak_memory,
331            peak_cpu_percent: cpu_peak,
332            total_operations: total_ops,
333            failed_operations: failed_ops,
334        }
335    }
336
337    /// Get constraints
338    pub fn constraints(&self) -> &ResourceConstraints {
339        &self.constraints
340    }
341
342    /// Reset metrics
343    pub fn reset_metrics(&self) {
344        self.total_ops.store(0, Ordering::Relaxed);
345        self.failed_ops.store(0, Ordering::Relaxed);
346        self.peak_memory.store(0, Ordering::Relaxed);
347        self.cpu_samples.write().clear();
348    }
349
350    /// Check overall health
351    pub fn health_check(&self) -> HealthStatus {
352        let metrics = self.metrics();
353
354        let memory_ok = metrics.memory_percent(self.constraints.max_memory_bytes) < 90.0;
355        let cpu_ok = metrics.cpu_percent < self.constraints.max_cpu_percent * 0.9;
356        let storage_ok = metrics.storage_percent(self.constraints.max_storage_bytes) < 90.0;
357        let success_ok = metrics.success_rate() > 95.0;
358
359        if memory_ok && cpu_ok && storage_ok && success_ok {
360            HealthStatus::Healthy
361        } else if !memory_ok || !cpu_ok || !storage_ok {
362            HealthStatus::Critical
363        } else {
364            HealthStatus::Degraded
365        }
366    }
367}
368
369/// Health status of edge device
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371pub enum HealthStatus {
372    /// All resources operating normally
373    Healthy,
374    /// Some resources under pressure but operational
375    Degraded,
376    /// Critical resource constraints
377    Critical,
378}
379
380/// RAII guard for operations
381pub struct OperationGuard {
382    active_ops: Arc<AtomicUsize>,
383}
384
385impl Drop for OperationGuard {
386    fn drop(&mut self) {
387        self.active_ops.fetch_sub(1, Ordering::Relaxed);
388    }
389}
390
391/// RAII guard for memory allocation
392pub struct MemoryGuard {
393    bytes: usize,
394    memory_used: Arc<AtomicUsize>,
395}
396
397impl Drop for MemoryGuard {
398    fn drop(&mut self) {
399        self.memory_used.fetch_sub(self.bytes, Ordering::Relaxed);
400    }
401}
402
403/// RAII guard for storage allocation
404pub struct StorageGuard {
405    bytes: usize,
406    storage_used: Arc<AtomicUsize>,
407}
408
409impl Drop for StorageGuard {
410    fn drop(&mut self) {
411        self.storage_used.fetch_sub(self.bytes, Ordering::Relaxed);
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_constraints_creation() {
421        let constraints = ResourceConstraints::default();
422        assert!(constraints.validate().is_ok());
423
424        let minimal = ResourceConstraints::minimal();
425        assert!(minimal.validate().is_ok());
426        assert!(minimal.max_memory_bytes < constraints.max_memory_bytes);
427    }
428
429    #[test]
430    fn test_constraints_validation() {
431        let mut invalid = ResourceConstraints {
432            max_memory_bytes: 0,
433            ..Default::default()
434        };
435        assert!(invalid.validate().is_err());
436
437        invalid.max_memory_bytes = 1024;
438        invalid.max_cpu_percent = 150.0;
439        assert!(invalid.validate().is_err());
440    }
441
442    #[test]
443    fn test_resource_manager_creation() {
444        let constraints = ResourceConstraints::minimal();
445        let manager = ResourceManager::new(constraints);
446        assert!(manager.is_ok());
447    }
448
449    #[test]
450    fn test_operation_guard() -> Result<()> {
451        let constraints = ResourceConstraints::minimal();
452        let manager = ResourceManager::new(constraints)?;
453
454        let metrics_before = manager.metrics();
455        assert_eq!(metrics_before.active_operations, 0);
456
457        {
458            let _guard = manager.start_operation()?;
459            let metrics_during = manager.metrics();
460            assert_eq!(metrics_during.active_operations, 1);
461        }
462
463        let metrics_after = manager.metrics();
464        assert_eq!(metrics_after.active_operations, 0);
465
466        Ok(())
467    }
468
469    #[test]
470    fn test_memory_allocation() -> Result<()> {
471        let constraints = ResourceConstraints::minimal();
472        let manager = ResourceManager::new(constraints)?;
473
474        let _guard = manager.allocate_memory(1024)?;
475        let metrics = manager.metrics();
476        assert_eq!(metrics.memory_bytes, 1024);
477
478        Ok(())
479    }
480
481    #[test]
482    fn test_memory_limit() -> Result<()> {
483        let mut constraints = ResourceConstraints::minimal();
484        constraints.max_memory_bytes = 2048;
485        let manager = ResourceManager::new(constraints)?;
486
487        let _guard1 = manager.allocate_memory(1024)?;
488        let _guard2 = manager.allocate_memory(512)?;
489
490        // This should fail
491        let result = manager.allocate_memory(1024);
492        assert!(result.is_err());
493
494        Ok(())
495    }
496
497    #[test]
498    fn test_concurrent_operations_limit() -> Result<()> {
499        let mut constraints = ResourceConstraints::minimal();
500        constraints.max_concurrent_ops = 2;
501        let manager = ResourceManager::new(constraints)?;
502
503        let _guard1 = manager.start_operation()?;
504        let _guard2 = manager.start_operation()?;
505
506        // This should fail
507        let result = manager.start_operation();
508        assert!(result.is_err());
509
510        Ok(())
511    }
512
513    #[test]
514    fn test_cpu_tracking() -> Result<()> {
515        let constraints = ResourceConstraints::minimal();
516        let manager = ResourceManager::new(constraints)?;
517
518        manager.record_cpu_sample(10.0);
519        manager.record_cpu_sample(20.0);
520        manager.record_cpu_sample(30.0);
521
522        let cpu = manager.current_cpu();
523        assert!((cpu - 20.0).abs() < 1.0);
524
525        Ok(())
526    }
527
528    #[test]
529    fn test_metrics() -> Result<()> {
530        let constraints = ResourceConstraints::minimal();
531        let manager = ResourceManager::new(constraints)?;
532
533        let _guard = manager.start_operation()?;
534        manager.record_failure();
535
536        let metrics = manager.metrics();
537        assert_eq!(metrics.active_operations, 1);
538        assert_eq!(metrics.total_operations, 1);
539        assert_eq!(metrics.failed_operations, 1);
540
541        Ok(())
542    }
543
544    #[test]
545    fn test_health_check() -> Result<()> {
546        let constraints = ResourceConstraints::minimal();
547        let max_memory = constraints.max_memory_bytes;
548        let manager = ResourceManager::new(constraints)?;
549
550        assert_eq!(manager.health_check(), HealthStatus::Healthy);
551
552        // Fill up memory
553        let _guard = manager.allocate_memory(max_memory - 100)?;
554        assert_eq!(manager.health_check(), HealthStatus::Critical);
555
556        Ok(())
557    }
558
559    #[test]
560    fn test_peak_memory() -> Result<()> {
561        let constraints = ResourceConstraints::minimal();
562        let manager = ResourceManager::new(constraints)?;
563
564        {
565            let _guard = manager.allocate_memory(1000)?;
566            let metrics = manager.metrics();
567            assert_eq!(metrics.peak_memory_bytes, 1000);
568        }
569
570        let metrics = manager.metrics();
571        assert_eq!(metrics.memory_bytes, 0);
572        assert_eq!(metrics.peak_memory_bytes, 1000);
573
574        Ok(())
575    }
576}