Skip to main content

nucleus/resources/
cgroup.rs

1use crate::error::{NucleusError, Result, StateTransition};
2use crate::resources::{CgroupState, ResourceLimits};
3use std::fs;
4use std::path::{Path, PathBuf};
5use tracing::{debug, info};
6
7const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
8
9/// Cgroup v2 manager
10///
11/// Implements the cgroup lifecycle state machine from
12/// Nucleus_Resources_CgroupLifecycle.tla
13pub struct Cgroup {
14    path: PathBuf,
15    state: CgroupState,
16}
17
18impl Cgroup {
19    /// Create a new cgroup with the given name
20    ///
21    /// State transition: Nonexistent -> Created
22    pub fn create(name: &str) -> Result<Self> {
23        let state = CgroupState::Nonexistent.transition(CgroupState::Created)?;
24        let path = PathBuf::from(CGROUP_V2_ROOT).join(name);
25
26        info!("Creating cgroup at {:?}", path);
27
28        // Create cgroup directory
29        fs::create_dir_all(&path).map_err(|e| {
30            NucleusError::CgroupError(format!("Failed to create cgroup directory: {}", e))
31        })?;
32
33        Ok(Self { path, state })
34    }
35
36    /// Set resource limits
37    ///
38    /// State transition: Created -> Configured
39    pub fn set_limits(&mut self, limits: &ResourceLimits) -> Result<()> {
40        self.state = self.state.transition(CgroupState::Configured)?;
41
42        info!("Configuring cgroup limits: {:?}", limits);
43
44        // Set memory limit
45        if let Some(memory_bytes) = limits.memory_bytes {
46            self.write_value("memory.max", &memory_bytes.to_string())?;
47            debug!("Set memory.max = {}", memory_bytes);
48        }
49
50        // Set memory soft limit (high watermark)
51        if let Some(memory_high) = limits.memory_high {
52            self.write_value("memory.high", &memory_high.to_string())?;
53            debug!("Set memory.high = {}", memory_high);
54        }
55
56        // Set swap limit
57        if let Some(swap_max) = limits.memory_swap_max {
58            self.write_value("memory.swap.max", &swap_max.to_string())?;
59            debug!("Set memory.swap.max = {}", swap_max);
60        }
61
62        // Set CPU limit
63        if let Some(cpu_quota_us) = limits.cpu_quota_us {
64            let cpu_max = format!("{} {}", cpu_quota_us, limits.cpu_period_us);
65            self.write_value("cpu.max", &cpu_max)?;
66            debug!("Set cpu.max = {}", cpu_max);
67        }
68
69        // Set CPU weight
70        if let Some(cpu_weight) = limits.cpu_weight {
71            self.write_value("cpu.weight", &cpu_weight.to_string())?;
72            debug!("Set cpu.weight = {}", cpu_weight);
73        }
74
75        // Set PID limit
76        if let Some(pids_max) = limits.pids_max {
77            self.write_value("pids.max", &pids_max.to_string())?;
78            debug!("Set pids.max = {}", pids_max);
79        }
80
81        // Set I/O limits
82        for io_limit in &limits.io_limits {
83            let line = io_limit.to_io_max_line();
84            self.write_value("io.max", &line)?;
85            debug!("Set io.max: {}", line);
86        }
87
88        info!("Successfully configured cgroup limits");
89
90        Ok(())
91    }
92
93    /// Attach a process to this cgroup
94    ///
95    /// State transition: Configured -> Attached
96    pub fn attach_process(&mut self, pid: u32) -> Result<()> {
97        self.state = self.state.transition(CgroupState::Attached)?;
98
99        info!("Attaching process {} to cgroup", pid);
100
101        self.write_value("cgroup.procs", &pid.to_string())?;
102
103        info!("Successfully attached process to cgroup");
104
105        Ok(())
106    }
107
108    /// Write a value to a cgroup file
109    fn write_value(&self, file: &str, value: &str) -> Result<()> {
110        let file_path = self.path.join(file);
111        fs::write(&file_path, value).map_err(|e| {
112            NucleusError::CgroupError(format!(
113                "Failed to write {} to {:?}: {}",
114                value, file_path, e
115            ))
116        })?;
117        Ok(())
118    }
119
120    /// Read a value from a cgroup file
121    fn read_value(&self, file: &str) -> Result<String> {
122        let file_path = self.path.join(file);
123        fs::read_to_string(&file_path).map_err(|e| {
124            NucleusError::CgroupError(format!("Failed to read {:?}: {}", file_path, e))
125        })
126    }
127
128    /// Get current memory usage
129    pub fn memory_current(&self) -> Result<u64> {
130        let value = self.read_value("memory.current")?;
131        value.trim().parse().map_err(|e| {
132            NucleusError::CgroupError(format!("Failed to parse memory.current: {}", e))
133        })
134    }
135
136    /// Get cgroup path
137    pub fn path(&self) -> &Path {
138        &self.path
139    }
140
141    /// Get the current state of this cgroup
142    pub fn state(&self) -> CgroupState {
143        self.state
144    }
145
146    /// Clean up the cgroup
147    ///
148    /// State transition: * -> Removed (only on success)
149    pub fn cleanup(mut self) -> Result<()> {
150        info!("Cleaning up cgroup {:?}", self.path);
151
152        // Try to remove the cgroup directory
153        // This will fail if there are still processes in the cgroup
154        if self.path.exists() {
155            fs::remove_dir(&self.path).map_err(|e| {
156                // BUG-06: Do NOT set state to Removed on failure – Drop should
157                // still attempt cleanup when the Cgroup is dropped.
158                NucleusError::CgroupError(format!("Failed to remove cgroup: {}", e))
159            })?;
160        }
161
162        // Only mark as terminal after successful removal
163        self.state = CgroupState::Removed;
164        info!("Successfully cleaned up cgroup");
165
166        Ok(())
167    }
168}
169
170impl Drop for Cgroup {
171    fn drop(&mut self) {
172        if !self.state.is_terminal() && self.path.exists() {
173            let _ = fs::remove_dir(&self.path);
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_resource_limits_unlimited() {
184        let limits = ResourceLimits::unlimited();
185        assert!(limits.memory_bytes.is_none());
186        assert!(limits.memory_high.is_none());
187        assert!(limits.memory_swap_max.is_none());
188        assert!(limits.cpu_quota_us.is_none());
189        assert!(limits.cpu_weight.is_none());
190        assert!(limits.pids_max.is_none());
191        assert!(limits.io_limits.is_empty());
192    }
193
194    // Note: Testing actual cgroup operations requires root privileges
195    // and cgroup v2 filesystem. These are tested in integration tests.
196
197    #[test]
198    fn test_cleanup_sets_removed_only_after_success() {
199        // BUG-06: cleanup must not mark state as Removed before the directory
200        // is actually removed. Verify structurally by brace-matching the
201        // function body instead of using a fragile char-window offset.
202        let source = include_str!("cgroup.rs");
203        let fn_start = source.find("pub fn cleanup").unwrap();
204        let after = &source[fn_start..];
205        let open = after.find('{').unwrap();
206        let mut depth = 0u32;
207        let mut fn_end = open;
208        for (i, ch) in after[open..].char_indices() {
209            match ch {
210                '{' => depth += 1,
211                '}' => {
212                    depth -= 1;
213                    if depth == 0 {
214                        fn_end = open + i + 1;
215                        break;
216                    }
217                }
218                _ => {}
219            }
220        }
221        let cleanup_body = &after[..fn_end];
222        let removed_pos = cleanup_body
223            .find("Removed")
224            .expect("must reference Removed state");
225        let remove_dir_pos = cleanup_body
226            .find("remove_dir")
227            .expect("must call remove_dir");
228        assert!(
229            removed_pos > remove_dir_pos,
230            "CgroupState::Removed must be set AFTER remove_dir succeeds, not before"
231        );
232    }
233}