1use crate::error::{NucleusError, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct IoDeviceLimit {
7 pub device: String,
9 pub riops: Option<u64>,
11 pub wiops: Option<u64>,
13 pub rbps: Option<u64>,
15 pub wbps: Option<u64>,
17}
18
19impl IoDeviceLimit {
20 pub fn parse(s: &str) -> Result<Self> {
22 let mut parts = s.split_whitespace();
23
24 let device = parts
25 .next()
26 .ok_or_else(|| NucleusError::InvalidResourceLimit("Empty I/O limit spec".into()))?;
27
28 let mut dev_parts = device.split(':');
30 let major = dev_parts.next().and_then(|s| s.parse::<u64>().ok());
31 let minor = dev_parts.next().and_then(|s| s.parse::<u64>().ok());
32 if major.is_none() || minor.is_none() || dev_parts.next().is_some() {
33 return Err(NucleusError::InvalidResourceLimit(format!(
34 "Invalid device format '{}', expected 'major:minor'",
35 device
36 )));
37 }
38
39 let mut limit = Self {
40 device: device.to_string(),
41 riops: None,
42 wiops: None,
43 rbps: None,
44 wbps: None,
45 };
46
47 for param in parts {
48 let (key, value) = param.split_once('=').ok_or_else(|| {
49 NucleusError::InvalidResourceLimit(format!(
50 "Invalid I/O param '{}', expected key=value",
51 param
52 ))
53 })?;
54 let value: u64 = value.parse().map_err(|_| {
55 NucleusError::InvalidResourceLimit(format!("Invalid I/O value: {}", value))
56 })?;
57
58 match key {
59 "riops" => limit.riops = Some(value),
60 "wiops" => limit.wiops = Some(value),
61 "rbps" => limit.rbps = Some(value),
62 "wbps" => limit.wbps = Some(value),
63 _ => {
64 return Err(NucleusError::InvalidResourceLimit(format!(
65 "Unknown I/O param '{}'",
66 key
67 )));
68 }
69 }
70 }
71
72 Ok(limit)
73 }
74
75 pub fn to_io_max_line(&self) -> String {
77 let mut parts = vec![self.device.clone()];
78 if let Some(v) = self.riops {
79 parts.push(format!("riops={}", v));
80 }
81 if let Some(v) = self.wiops {
82 parts.push(format!("wiops={}", v));
83 }
84 if let Some(v) = self.rbps {
85 parts.push(format!("rbps={}", v));
86 }
87 if let Some(v) = self.wbps {
88 parts.push(format!("wbps={}", v));
89 }
90 parts.join(" ")
91 }
92}
93
94#[derive(Debug, Clone, Serialize)]
96pub struct ResourceLimits {
97 pub memory_bytes: Option<u64>,
99 pub memory_high: Option<u64>,
101 pub memory_swap_max: Option<u64>,
103 pub cpu_quota_us: Option<u64>,
105 pub cpu_period_us: u64,
107 pub cpu_weight: Option<u64>,
109 pub pids_max: Option<u64>,
111 pub io_limits: Vec<IoDeviceLimit>,
113 pub memlock_bytes: Option<u64>,
116}
117
118impl ResourceLimits {
119 pub fn unlimited() -> Self {
121 Self {
122 memory_bytes: None,
123 memory_high: None,
124 memory_swap_max: None,
125 cpu_quota_us: None,
126 cpu_period_us: 100_000, cpu_weight: None,
128 pids_max: None,
129 io_limits: Vec::new(),
130 memlock_bytes: None,
131 }
132 }
133
134 pub fn parse_memory(s: &str) -> Result<u64> {
136 let s = s.trim();
137 if s.is_empty() {
138 return Err(NucleusError::InvalidResourceLimit(
139 "Empty memory limit".to_string(),
140 ));
141 }
142
143 let (num_str, multiplier) = if s.ends_with('K') || s.ends_with('k') {
144 (&s[..s.len() - 1], 1024u64)
145 } else if s.ends_with('M') || s.ends_with('m') {
146 (&s[..s.len() - 1], 1024 * 1024)
147 } else if s.ends_with('G') || s.ends_with('g') {
148 (&s[..s.len() - 1], 1024 * 1024 * 1024)
149 } else if s.ends_with('T') || s.ends_with('t') {
150 (&s[..s.len() - 1], 1024 * 1024 * 1024 * 1024)
151 } else {
152 (s, 1)
154 };
155
156 let num: u64 = num_str.parse().map_err(|_| {
157 NucleusError::InvalidResourceLimit(format!("Invalid memory value: {}", s))
158 })?;
159
160 num.checked_mul(multiplier).ok_or_else(|| {
161 NucleusError::InvalidResourceLimit(format!("Memory value overflows u64: {}", s))
162 })
163 }
164
165 pub fn with_memory(mut self, limit: &str) -> Result<Self> {
170 let bytes = Self::parse_memory(limit)?;
171 self.memory_bytes = Some(bytes);
172 self.memory_high = Some(bytes - bytes / 10);
174 if self.memory_swap_max.is_none() {
176 self.memory_swap_max = Some(0);
177 }
178 Ok(self)
179 }
180
181 pub fn with_swap_enabled(mut self) -> Self {
183 self.memory_swap_max = None;
184 self
185 }
186
187 pub fn with_cpu_cores(mut self, cores: f64) -> Result<Self> {
189 const MAX_CPU_CORES: f64 = 65_536.0;
190
191 if self.cpu_period_us == 0 {
192 return Err(NucleusError::InvalidResourceLimit(
193 "CPU period must be greater than 0".to_string(),
194 ));
195 }
196 if cores <= 0.0 || cores.is_nan() || cores.is_infinite() {
197 return Err(NucleusError::InvalidResourceLimit(
198 "CPU cores must be a finite positive number".to_string(),
199 ));
200 }
201 if cores > MAX_CPU_CORES {
202 return Err(NucleusError::InvalidResourceLimit(format!(
203 "CPU cores must be <= {}",
204 MAX_CPU_CORES
205 )));
206 }
207 let quota = (cores * self.cpu_period_us as f64) as u64;
209 self.cpu_quota_us = Some(quota);
210 Ok(self)
211 }
212
213 pub fn with_pids(mut self, max_pids: u64) -> Result<Self> {
215 if max_pids == 0 {
216 return Err(NucleusError::InvalidResourceLimit(
217 "Max PIDs must be positive".to_string(),
218 ));
219 }
220 if max_pids == libc::RLIM_INFINITY {
221 return Err(NucleusError::InvalidResourceLimit(
222 "Max PIDs must be less than RLIM_INFINITY; use 0/None for unlimited".to_string(),
223 ));
224 }
225 self.pids_max = Some(max_pids);
226 Ok(self)
227 }
228
229 pub fn with_cpu_weight(mut self, weight: u64) -> Result<Self> {
231 if !(1..=10000).contains(&weight) {
232 return Err(NucleusError::InvalidResourceLimit(
233 "CPU weight must be between 1 and 10000".to_string(),
234 ));
235 }
236 self.cpu_weight = Some(weight);
237 Ok(self)
238 }
239
240 pub fn with_io_limit(mut self, limit: IoDeviceLimit) -> Self {
242 self.io_limits.push(limit);
243 self
244 }
245
246 pub fn with_memlock(mut self, limit: &str) -> Result<Self> {
248 self.memlock_bytes = Some(Self::parse_memory(limit)?);
249 Ok(self)
250 }
251
252 pub fn validate_runtime_sanity(&self) -> Result<()> {
254 if self.cpu_period_us == 0 {
255 return Err(NucleusError::InvalidResourceLimit(
256 "CPU period must be greater than 0".to_string(),
257 ));
258 }
259
260 if let Some(pids_max) = self.pids_max {
261 if pids_max == 0 {
262 return Err(NucleusError::InvalidResourceLimit(
263 "Max PIDs must be positive".to_string(),
264 ));
265 }
266 if pids_max == libc::RLIM_INFINITY {
267 return Err(NucleusError::InvalidResourceLimit(
268 "Max PIDs must be less than RLIM_INFINITY; use 0/None for unlimited"
269 .to_string(),
270 ));
271 }
272 }
273
274 Ok(())
275 }
276
277 pub fn has_cgroup_control(&self) -> bool {
279 self.memory_bytes.is_some()
280 || self.memory_high.is_some()
281 || self.memory_swap_max.is_some()
282 || self.cpu_quota_us.is_some()
283 || self.cpu_weight.is_some()
284 || self.pids_max.is_some()
285 || !self.io_limits.is_empty()
286 }
287}
288
289impl Default for ResourceLimits {
290 fn default() -> Self {
291 Self {
292 pids_max: Some(512),
293 ..Self::unlimited()
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_parse_memory() {
304 assert_eq!(ResourceLimits::parse_memory("1024").unwrap(), 1024);
305 assert_eq!(ResourceLimits::parse_memory("512K").unwrap(), 512 * 1024);
306 assert_eq!(
307 ResourceLimits::parse_memory("512M").unwrap(),
308 512 * 1024 * 1024
309 );
310 assert_eq!(
311 ResourceLimits::parse_memory("2G").unwrap(),
312 2 * 1024 * 1024 * 1024
313 );
314 }
315
316 #[test]
317 fn test_parse_memory_invalid() {
318 assert!(ResourceLimits::parse_memory("").is_err());
319 assert!(ResourceLimits::parse_memory("abc").is_err());
320 assert!(ResourceLimits::parse_memory("M").is_err());
321 }
322
323 #[test]
324 fn test_parse_memory_overflow_rejected() {
325 assert!(ResourceLimits::parse_memory("99999999999999T").is_err());
327 assert!(ResourceLimits::parse_memory("16383P").is_err()); }
330
331 #[test]
332 fn test_with_cpu_cores() {
333 let limits = ResourceLimits::unlimited();
334 let limits = limits.with_cpu_cores(2.0).unwrap();
335 assert_eq!(limits.cpu_quota_us, Some(200_000)); }
337
338 #[test]
339 fn test_with_cpu_cores_fractional() {
340 let limits = ResourceLimits::unlimited();
341 let limits = limits.with_cpu_cores(0.5).unwrap();
342 assert_eq!(limits.cpu_quota_us, Some(50_000)); }
344
345 #[test]
346 fn test_with_cpu_cores_invalid() {
347 let limits = ResourceLimits::unlimited();
348 assert!(limits.with_cpu_cores(0.0).is_err());
349 assert!(ResourceLimits::unlimited().with_cpu_cores(-1.0).is_err());
350 }
351
352 #[test]
353 fn test_with_cpu_cores_rejects_zero_period() {
354 let mut limits = ResourceLimits::unlimited();
355 limits.cpu_period_us = 0;
356 assert!(limits.with_cpu_cores(1.0).is_err());
357 }
358
359 #[test]
360 fn test_with_memory_auto_sets_memory_high() {
361 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
362 let expected_bytes = 1024 * 1024 * 1024u64;
363 assert_eq!(limits.memory_bytes, Some(expected_bytes));
364 assert_eq!(
366 limits.memory_high,
367 Some(expected_bytes - expected_bytes / 10)
368 );
369 }
370
371 #[test]
372 fn test_with_memory_disables_swap_by_default() {
373 let limits = ResourceLimits::unlimited().with_memory("512M").unwrap();
374 assert_eq!(limits.memory_swap_max, Some(0));
375 }
376
377 #[test]
378 fn test_swap_enabled_clears_swap_limit() {
379 let limits = ResourceLimits::unlimited()
380 .with_memory("512M")
381 .unwrap()
382 .with_swap_enabled();
383 assert!(limits.memory_swap_max.is_none());
384 }
385
386 #[test]
387 fn test_with_cpu_weight_valid() {
388 let limits = ResourceLimits::unlimited().with_cpu_weight(100).unwrap();
389 assert_eq!(limits.cpu_weight, Some(100));
390
391 let limits = ResourceLimits::unlimited().with_cpu_weight(1).unwrap();
392 assert_eq!(limits.cpu_weight, Some(1));
393
394 let limits = ResourceLimits::unlimited().with_cpu_weight(10000).unwrap();
395 assert_eq!(limits.cpu_weight, Some(10000));
396 }
397
398 #[test]
399 fn test_with_cpu_weight_invalid() {
400 assert!(ResourceLimits::unlimited().with_cpu_weight(0).is_err());
401 assert!(ResourceLimits::unlimited().with_cpu_weight(10001).is_err());
402 }
403
404 #[test]
405 fn test_with_pids_rejects_rlim_infinity() {
406 assert!(ResourceLimits::unlimited()
407 .with_pids(libc::RLIM_INFINITY)
408 .is_err());
409 }
410
411 #[test]
412 fn test_io_device_limit_parse_valid() {
413 let limit = IoDeviceLimit::parse("8:0 riops=1000 wbps=10485760").unwrap();
414 assert_eq!(limit.device, "8:0");
415 assert_eq!(limit.riops, Some(1000));
416 assert_eq!(limit.wbps, Some(10485760));
417 assert!(limit.wiops.is_none());
418 assert!(limit.rbps.is_none());
419 }
420
421 #[test]
422 fn test_io_device_limit_parse_all_params() {
423 let limit = IoDeviceLimit::parse("8:0 riops=100 wiops=200 rbps=300 wbps=400").unwrap();
424 assert_eq!(limit.riops, Some(100));
425 assert_eq!(limit.wiops, Some(200));
426 assert_eq!(limit.rbps, Some(300));
427 assert_eq!(limit.wbps, Some(400));
428 }
429
430 #[test]
431 fn test_io_device_limit_parse_invalid() {
432 assert!(IoDeviceLimit::parse("").is_err());
434 assert!(IoDeviceLimit::parse("bad").is_err());
436 assert!(IoDeviceLimit::parse("8:0:1").is_err());
437 assert!(IoDeviceLimit::parse("8:0 riops").is_err());
439 assert!(IoDeviceLimit::parse("8:0 foo=100").is_err());
441 assert!(IoDeviceLimit::parse("8:0 riops=abc").is_err());
443 }
444
445 #[test]
446 fn test_io_device_limit_to_io_max_line() {
447 let limit = IoDeviceLimit {
448 device: "8:0".to_string(),
449 riops: Some(1000),
450 wiops: None,
451 rbps: None,
452 wbps: Some(10485760),
453 };
454 assert_eq!(limit.to_io_max_line(), "8:0 riops=1000 wbps=10485760");
455 }
456
457 #[test]
458 fn test_unlimited_defaults() {
459 let limits = ResourceLimits::unlimited();
460 assert!(limits.memory_bytes.is_none());
461 assert!(limits.memory_high.is_none());
462 assert!(limits.memory_swap_max.is_none());
463 assert!(limits.cpu_quota_us.is_none());
464 assert!(limits.cpu_weight.is_none());
465 assert!(limits.pids_max.is_none());
466 assert!(limits.io_limits.is_empty());
467 assert!(!limits.has_cgroup_control());
468 assert!(ResourceLimits::default().has_cgroup_control());
469 }
470
471 #[test]
472 fn test_memory_high_uses_integer_arithmetic() {
473 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
475 let bytes = 1024u64 * 1024 * 1024;
476 let expected_high = bytes - bytes / 10; assert_eq!(
478 limits.memory_high,
479 Some(expected_high),
480 "memory_high must be exactly bytes - bytes/10 (integer arithmetic)"
481 );
482 }
483
484 #[test]
485 fn test_cpu_cores_rejects_extreme_values() {
486 assert!(ResourceLimits::unlimited()
488 .with_cpu_cores(f64::NAN)
489 .is_err());
490 assert!(ResourceLimits::unlimited()
491 .with_cpu_cores(f64::INFINITY)
492 .is_err());
493 assert!(
494 ResourceLimits::unlimited()
495 .with_cpu_cores(100_000.0)
496 .is_err(),
497 "CPU cores > 65536 must be rejected to prevent quota overflow"
498 );
499 }
500
501 #[test]
502 fn test_validate_runtime_sanity_rejects_invalid_deserialized_values() {
503 let mut limits = ResourceLimits::unlimited();
504 limits.cpu_period_us = 0;
505 limits.pids_max = Some(libc::RLIM_INFINITY);
506
507 assert!(limits.validate_runtime_sanity().is_err());
508 }
509}