mecha10-node-runner 0.8.0

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;

#[derive(Debug, Clone)]
pub struct ResourceLimits {
    pub memory_mb: Option<u64>,
    pub cpu_percent: Option<f32>,
}

pub struct ResourceMonitor {
    system: System,
    pid: SysinfoPid,
    limits: ResourceLimits,
    memory_warned: bool,
    cpu_warned: bool,
}

#[derive(Debug)]
pub enum ResourceStatus {
    Ok,
    Exceeded {
        resource: &'static str,
        current: u64,
        limit: u64,
    },
    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,
            memory_warned: false,
            cpu_warned: false,
        }
    }

    /// 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> {
        self.system
            .refresh_processes(ProcessesToUpdate::Some(&[self.pid]), false);

        if let Some(process) = self.system.process(self.pid) {
            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) {
                    if !self.memory_warned {
                        warn!(
                            "Memory usage approaching limit: {} MB / {} MB ({:.1}%)",
                            memory_mb,
                            limit_mb,
                            (memory_mb as f64 / limit_mb as f64) * 100.0
                        );
                        self.memory_warned = true;
                    }
                } else {
                    self.memory_warned = false;
                }
            }

            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) {
                    if !self.cpu_warned {
                        warn!("CPU usage approaching limit: {:.1}% / {:.1}%", cpu_usage, limit_percent);
                        self.cpu_warned = true;
                    }
                } else {
                    self.cpu_warned = false;
                }
            }

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

    /// Get memory usage in megabytes
    fn get_memory_usage_mb(&self, process: &Process) -> u64 {
        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());
    }
}