mecha10-node-runner 0.6.3

Node lifecycle manager for Mecha10 - handles pre-flight checks and node execution
//! Resource limit monitoring for node processes
//!
//! This module provides resource monitoring and limit enforcement:
//! - Memory usage tracking
//! - CPU usage tracking
//! - Warning when approaching limits (>80%)
//! - Process termination when exceeding limits
//!
//! Uses the `sysinfo` crate for cross-platform resource monitoring.

use anyhow::Result;
use sysinfo::{Pid as SysinfoPid, Process, ProcessesToUpdate, System};
use tracing::warn;

/// Resource limit configuration
#[derive(Debug, Clone)]
pub struct ResourceLimits {
    /// Memory limit in megabytes
    pub memory_mb: Option<u64>,
    /// CPU usage limit as percentage (0-100)
    pub cpu_percent: Option<f32>,
}

/// Resource usage monitor
pub struct ResourceMonitor {
    system: System,
    pid: SysinfoPid,
    limits: ResourceLimits,
}

/// Resource check status
#[derive(Debug)]
pub enum ResourceStatus {
    /// Resources within limits
    Ok,
    /// Resource limit exceeded
    Exceeded {
        resource: &'static str,
        current: u64,
        limit: u64,
    },
    /// Process not found (likely exited)
    ProcessNotFound,
}

impl ResourceMonitor {
    /// Create a new resource monitor for a process
    ///
    /// # Arguments
    ///
    /// * `pid` - Process ID to monitor
    /// * `limits` - Resource limits to enforce
    pub fn new(pid: u32, limits: ResourceLimits) -> Self {
        Self {
            system: System::new(),
            pid: SysinfoPid::from_u32(pid),
            limits,
        }
    }

    /// Check resource usage against limits
    ///
    /// Returns:
    /// - `ResourceStatus::Ok` if within limits
    /// - `ResourceStatus::Exceeded` if any limit is exceeded
    /// - `ResourceStatus::ProcessNotFound` if process doesn't exist
    pub fn check_resources(&mut self) -> Result<ResourceStatus> {
        // Refresh specific process (sysinfo 0.32 API)
        self.system
            .refresh_processes(ProcessesToUpdate::Some(&[self.pid]), false);

        if let Some(process) = self.system.process(self.pid) {
            // Check memory limit
            if let Some(limit_mb) = self.limits.memory_mb {
                let memory_mb = self.get_memory_usage_mb(process);

                if memory_mb > limit_mb {
                    return Ok(ResourceStatus::Exceeded {
                        resource: "memory",
                        current: memory_mb,
                        limit: limit_mb,
                    });
                } else if memory_mb > (limit_mb * 80 / 100) {
                    warn!(
                        "Memory usage approaching limit: {} MB / {} MB ({:.1}%)",
                        memory_mb,
                        limit_mb,
                        (memory_mb as f64 / limit_mb as f64) * 100.0
                    );
                }
            }

            // Check CPU limit
            if let Some(limit_percent) = self.limits.cpu_percent {
                let cpu_usage = process.cpu_usage();

                if cpu_usage > limit_percent {
                    return Ok(ResourceStatus::Exceeded {
                        resource: "cpu",
                        current: cpu_usage as u64,
                        limit: limit_percent as u64,
                    });
                } else if cpu_usage > (limit_percent * 0.8) {
                    warn!("CPU usage approaching limit: {:.1}% / {:.1}%", cpu_usage, limit_percent);
                }
            }

            Ok(ResourceStatus::Ok)
        } else {
            Ok(ResourceStatus::ProcessNotFound)
        }
    }

    /// Get memory usage in megabytes
    fn get_memory_usage_mb(&self, process: &Process) -> u64 {
        // Memory is returned in bytes, convert to MB
        process.memory() / 1024 / 1024
    }
}

impl ResourceLimits {
    /// Check if any limits are configured
    pub fn has_limits(&self) -> bool {
        self.memory_mb.is_some() || self.cpu_percent.is_some()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_resource_limits_none() {
        let limits = ResourceLimits {
            memory_mb: None,
            cpu_percent: None,
        };
        assert!(!limits.has_limits());
    }

    #[test]
    fn test_resource_limits_with_memory() {
        let limits = ResourceLimits {
            memory_mb: Some(512),
            cpu_percent: None,
        };
        assert!(limits.has_limits());
    }

    #[test]
    fn test_resource_limits_with_cpu() {
        let limits = ResourceLimits {
            memory_mb: None,
            cpu_percent: Some(50.0),
        };
        assert!(limits.has_limits());
    }
}