cellos_supervisor/
linux_cgroup.rs1use cellos_core::types::RunLimits;
19
20pub const DEFAULT_CPU_PERIOD_MICROS: u64 = 100_000;
24
25pub fn cpu_max_to_write(
49 spec_limits: Option<&RunLimits>,
50 env_cpu_max: Option<&str>,
51) -> Option<String> {
52 if let Some(cpu) = spec_limits.and_then(|l| l.cpu_max.as_ref()) {
54 if cpu.quota_micros == 0 {
55 return None;
57 }
58 let period = cpu
59 .period_micros
60 .unwrap_or(DEFAULT_CPU_PERIOD_MICROS)
61 .max(1);
62 let quota = cpu.quota_micros;
63 return Some(format!("{quota} {period}"));
64 }
65 let raw = env_cpu_max?.trim();
67 if raw.is_empty() {
68 return None;
69 }
70 normalize_env_cpu_max(raw)
71}
72
73fn normalize_env_cpu_max(raw: &str) -> Option<String> {
77 let mut parts = raw.split_ascii_whitespace();
78 let first = parts.next()?;
79 let second = parts.next();
80 if parts.next().is_some() {
81 return None; }
83 let quota = match first {
84 "max" => "max".to_string(),
85 n => {
86 let v: u64 = n.parse().ok()?;
87 if v == 0 {
88 return None;
89 }
90 v.to_string()
91 }
92 };
93 let period: u64 = match second {
94 Some(p) => {
95 let v: u64 = p.parse().ok()?;
96 if v == 0 {
97 return None;
98 }
99 v
100 }
101 None => DEFAULT_CPU_PERIOD_MICROS,
102 };
103 Some(format!("{quota} {period}"))
104}
105
106pub fn cpu_max_env_validation_error(env_cpu_max: Option<&str>) -> Option<&'static str> {
110 let raw = env_cpu_max?.trim();
111 if raw.is_empty() {
112 return None;
113 }
114 if normalize_env_cpu_max(raw).is_none() {
115 Some("expected `max`, `<quota_micros>`, `max <period_micros>`, or `<quota_micros> <period_micros>` (each integer >= 1)")
116 } else {
117 None
118 }
119}
120
121#[derive(Debug, PartialEq, Eq)]
123pub enum CpuMaxApplyOutcome {
124 Wrote {
127 source: CpuMaxSource,
129 payload: String,
131 },
132 Skipped,
134 InvalidEnvOverride,
136 WriteError(String),
139}
140
141#[derive(Debug, PartialEq, Eq, Clone, Copy)]
143pub enum CpuMaxSource {
144 Spec,
146 EnvOverride,
148}
149
150pub fn apply_cpu_max_to_leaf(
160 leaf: &std::path::Path,
161 spec_limits: Option<&RunLimits>,
162 env_cpu_max: Option<&str>,
163) -> CpuMaxApplyOutcome {
164 if let Some(payload) = cpu_max_to_write(spec_limits, env_cpu_max) {
165 let source = if spec_limits.and_then(|l| l.cpu_max.as_ref()).is_some() {
166 CpuMaxSource::Spec
167 } else {
168 CpuMaxSource::EnvOverride
169 };
170 let target = leaf.join("cpu.max");
171 match std::fs::write(&target, format!("{payload}\n")) {
172 Ok(()) => CpuMaxApplyOutcome::Wrote { source, payload },
173 Err(e) => CpuMaxApplyOutcome::WriteError(format!("{}: {e}", target.display())),
174 }
175 } else if spec_limits.and_then(|l| l.cpu_max.as_ref()).is_none()
176 && cpu_max_env_validation_error(env_cpu_max).is_some()
177 {
178 CpuMaxApplyOutcome::InvalidEnvOverride
179 } else {
180 CpuMaxApplyOutcome::Skipped
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use cellos_core::types::{RunCpuMax, RunLimits};
188
189 fn limits_with_cpu(quota: u64, period: Option<u64>) -> RunLimits {
190 RunLimits {
191 memory_max_bytes: None,
192 cpu_max: Some(RunCpuMax {
193 quota_micros: quota,
194 period_micros: period,
195 }),
196 graceful_shutdown_seconds: None,
197 }
198 }
199
200 #[test]
201 fn spec_with_quota_and_period_wins() {
202 let l = limits_with_cpu(50_000, Some(100_000));
203 assert_eq!(
204 cpu_max_to_write(Some(&l), Some("max 200000")),
205 Some("50000 100000".to_string())
206 );
207 }
208
209 #[test]
210 fn spec_quota_only_uses_default_period() {
211 let l = limits_with_cpu(75_000, None);
212 assert_eq!(
213 cpu_max_to_write(Some(&l), None),
214 Some("75000 100000".to_string())
215 );
216 }
217
218 #[test]
219 fn spec_quota_zero_returns_none_defense_in_depth() {
220 let l = limits_with_cpu(0, None);
221 assert_eq!(cpu_max_to_write(Some(&l), Some("50000 100000")), None);
222 }
223
224 #[test]
225 fn env_override_used_when_spec_absent() {
226 assert_eq!(
227 cpu_max_to_write(None, Some("50000 100000")),
228 Some("50000 100000".to_string())
229 );
230 }
231
232 #[test]
233 fn env_max_alone_normalizes_with_default_period() {
234 assert_eq!(
235 cpu_max_to_write(None, Some("max")),
236 Some("max 100000".to_string())
237 );
238 }
239
240 #[test]
241 fn env_max_with_period() {
242 assert_eq!(
243 cpu_max_to_write(None, Some("max 200000")),
244 Some("max 200000".to_string())
245 );
246 }
247
248 #[test]
249 fn env_quota_alone_uses_default_period() {
250 assert_eq!(
251 cpu_max_to_write(None, Some("25000")),
252 Some("25000 100000".to_string())
253 );
254 }
255
256 #[test]
257 fn env_whitespace_collapsed() {
258 assert_eq!(
259 cpu_max_to_write(None, Some(" 50000\t 100000 ")),
260 Some("50000 100000".to_string())
261 );
262 }
263
264 #[test]
265 fn env_invalid_returns_none() {
266 assert_eq!(cpu_max_to_write(None, Some("not-a-number")), None);
267 assert_eq!(cpu_max_to_write(None, Some("0 100000")), None);
268 assert_eq!(cpu_max_to_write(None, Some("50000 0")), None);
269 assert_eq!(cpu_max_to_write(None, Some("50000 100000 200000")), None);
270 assert_eq!(cpu_max_to_write(None, Some("-50000")), None);
271 }
272
273 #[test]
274 fn env_empty_treated_as_unset() {
275 assert_eq!(cpu_max_to_write(None, Some("")), None);
276 assert_eq!(cpu_max_to_write(None, Some(" ")), None);
277 assert_eq!(cpu_max_to_write(None, None), None);
278 }
279
280 #[test]
281 fn env_validation_error_diagnostic() {
282 assert!(cpu_max_env_validation_error(Some("not-a-number")).is_some());
283 assert!(cpu_max_env_validation_error(Some("0")).is_some());
284 assert!(cpu_max_env_validation_error(Some("50000 100000")).is_none());
285 assert!(cpu_max_env_validation_error(Some("max")).is_none());
286 assert!(cpu_max_env_validation_error(None).is_none());
287 assert!(cpu_max_env_validation_error(Some("")).is_none());
288 }
289
290 #[test]
293 fn spec_overrides_env_when_both_present() {
294 let l = limits_with_cpu(10_000, Some(50_000));
295 assert_eq!(
296 cpu_max_to_write(Some(&l), Some("max 200000")),
297 Some("10000 50000".to_string())
298 );
299 }
300}