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)]
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
278impl Default for ResourceLimits {
279 fn default() -> Self {
280 Self {
281 pids_max: Some(512),
282 ..Self::unlimited()
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_parse_memory() {
293 assert_eq!(ResourceLimits::parse_memory("1024").unwrap(), 1024);
294 assert_eq!(ResourceLimits::parse_memory("512K").unwrap(), 512 * 1024);
295 assert_eq!(
296 ResourceLimits::parse_memory("512M").unwrap(),
297 512 * 1024 * 1024
298 );
299 assert_eq!(
300 ResourceLimits::parse_memory("2G").unwrap(),
301 2 * 1024 * 1024 * 1024
302 );
303 }
304
305 #[test]
306 fn test_parse_memory_invalid() {
307 assert!(ResourceLimits::parse_memory("").is_err());
308 assert!(ResourceLimits::parse_memory("abc").is_err());
309 assert!(ResourceLimits::parse_memory("M").is_err());
310 }
311
312 #[test]
313 fn test_parse_memory_overflow_rejected() {
314 assert!(ResourceLimits::parse_memory("99999999999999T").is_err());
316 assert!(ResourceLimits::parse_memory("16383P").is_err()); }
319
320 #[test]
321 fn test_with_cpu_cores() {
322 let limits = ResourceLimits::unlimited();
323 let limits = limits.with_cpu_cores(2.0).unwrap();
324 assert_eq!(limits.cpu_quota_us, Some(200_000)); }
326
327 #[test]
328 fn test_with_cpu_cores_fractional() {
329 let limits = ResourceLimits::unlimited();
330 let limits = limits.with_cpu_cores(0.5).unwrap();
331 assert_eq!(limits.cpu_quota_us, Some(50_000)); }
333
334 #[test]
335 fn test_with_cpu_cores_invalid() {
336 let limits = ResourceLimits::unlimited();
337 assert!(limits.with_cpu_cores(0.0).is_err());
338 assert!(ResourceLimits::unlimited().with_cpu_cores(-1.0).is_err());
339 }
340
341 #[test]
342 fn test_with_cpu_cores_rejects_zero_period() {
343 let mut limits = ResourceLimits::unlimited();
344 limits.cpu_period_us = 0;
345 assert!(limits.with_cpu_cores(1.0).is_err());
346 }
347
348 #[test]
349 fn test_with_memory_auto_sets_memory_high() {
350 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
351 let expected_bytes = 1024 * 1024 * 1024u64;
352 assert_eq!(limits.memory_bytes, Some(expected_bytes));
353 assert_eq!(
355 limits.memory_high,
356 Some(expected_bytes - expected_bytes / 10)
357 );
358 }
359
360 #[test]
361 fn test_with_memory_disables_swap_by_default() {
362 let limits = ResourceLimits::unlimited().with_memory("512M").unwrap();
363 assert_eq!(limits.memory_swap_max, Some(0));
364 }
365
366 #[test]
367 fn test_swap_enabled_clears_swap_limit() {
368 let limits = ResourceLimits::unlimited()
369 .with_memory("512M")
370 .unwrap()
371 .with_swap_enabled();
372 assert!(limits.memory_swap_max.is_none());
373 }
374
375 #[test]
376 fn test_with_cpu_weight_valid() {
377 let limits = ResourceLimits::unlimited().with_cpu_weight(100).unwrap();
378 assert_eq!(limits.cpu_weight, Some(100));
379
380 let limits = ResourceLimits::unlimited().with_cpu_weight(1).unwrap();
381 assert_eq!(limits.cpu_weight, Some(1));
382
383 let limits = ResourceLimits::unlimited().with_cpu_weight(10000).unwrap();
384 assert_eq!(limits.cpu_weight, Some(10000));
385 }
386
387 #[test]
388 fn test_with_cpu_weight_invalid() {
389 assert!(ResourceLimits::unlimited().with_cpu_weight(0).is_err());
390 assert!(ResourceLimits::unlimited().with_cpu_weight(10001).is_err());
391 }
392
393 #[test]
394 fn test_with_pids_rejects_rlim_infinity() {
395 assert!(ResourceLimits::unlimited()
396 .with_pids(libc::RLIM_INFINITY)
397 .is_err());
398 }
399
400 #[test]
401 fn test_io_device_limit_parse_valid() {
402 let limit = IoDeviceLimit::parse("8:0 riops=1000 wbps=10485760").unwrap();
403 assert_eq!(limit.device, "8:0");
404 assert_eq!(limit.riops, Some(1000));
405 assert_eq!(limit.wbps, Some(10485760));
406 assert!(limit.wiops.is_none());
407 assert!(limit.rbps.is_none());
408 }
409
410 #[test]
411 fn test_io_device_limit_parse_all_params() {
412 let limit = IoDeviceLimit::parse("8:0 riops=100 wiops=200 rbps=300 wbps=400").unwrap();
413 assert_eq!(limit.riops, Some(100));
414 assert_eq!(limit.wiops, Some(200));
415 assert_eq!(limit.rbps, Some(300));
416 assert_eq!(limit.wbps, Some(400));
417 }
418
419 #[test]
420 fn test_io_device_limit_parse_invalid() {
421 assert!(IoDeviceLimit::parse("").is_err());
423 assert!(IoDeviceLimit::parse("bad").is_err());
425 assert!(IoDeviceLimit::parse("8:0:1").is_err());
426 assert!(IoDeviceLimit::parse("8:0 riops").is_err());
428 assert!(IoDeviceLimit::parse("8:0 foo=100").is_err());
430 assert!(IoDeviceLimit::parse("8:0 riops=abc").is_err());
432 }
433
434 #[test]
435 fn test_io_device_limit_to_io_max_line() {
436 let limit = IoDeviceLimit {
437 device: "8:0".to_string(),
438 riops: Some(1000),
439 wiops: None,
440 rbps: None,
441 wbps: Some(10485760),
442 };
443 assert_eq!(limit.to_io_max_line(), "8:0 riops=1000 wbps=10485760");
444 }
445
446 #[test]
447 fn test_unlimited_defaults() {
448 let limits = ResourceLimits::unlimited();
449 assert!(limits.memory_bytes.is_none());
450 assert!(limits.memory_high.is_none());
451 assert!(limits.memory_swap_max.is_none());
452 assert!(limits.cpu_quota_us.is_none());
453 assert!(limits.cpu_weight.is_none());
454 assert!(limits.pids_max.is_none());
455 assert!(limits.io_limits.is_empty());
456 }
457
458 #[test]
459 fn test_memory_high_uses_integer_arithmetic() {
460 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
462 let bytes = 1024u64 * 1024 * 1024;
463 let expected_high = bytes - bytes / 10; assert_eq!(
465 limits.memory_high,
466 Some(expected_high),
467 "memory_high must be exactly bytes - bytes/10 (integer arithmetic)"
468 );
469 }
470
471 #[test]
472 fn test_cpu_cores_rejects_extreme_values() {
473 assert!(ResourceLimits::unlimited()
475 .with_cpu_cores(f64::NAN)
476 .is_err());
477 assert!(ResourceLimits::unlimited()
478 .with_cpu_cores(f64::INFINITY)
479 .is_err());
480 assert!(
481 ResourceLimits::unlimited()
482 .with_cpu_cores(100_000.0)
483 .is_err(),
484 "CPU cores > 65536 must be rejected to prevent quota overflow"
485 );
486 }
487
488 #[test]
489 fn test_validate_runtime_sanity_rejects_invalid_deserialized_values() {
490 let mut limits = ResourceLimits::unlimited();
491 limits.cpu_period_us = 0;
492 limits.pids_max = Some(libc::RLIM_INFINITY);
493
494 assert!(limits.validate_runtime_sanity().is_err());
495 }
496}