1use crate::error::{NucleusError, Result};
2
3#[derive(Debug, Clone)]
5pub struct IoDeviceLimit {
6 pub device: String,
8 pub riops: Option<u64>,
10 pub wiops: Option<u64>,
12 pub rbps: Option<u64>,
14 pub wbps: Option<u64>,
16}
17
18impl IoDeviceLimit {
19 pub fn parse(s: &str) -> Result<Self> {
21 let mut parts = s.split_whitespace();
22
23 let device = parts
24 .next()
25 .ok_or_else(|| NucleusError::InvalidResourceLimit("Empty I/O limit spec".into()))?;
26
27 let mut dev_parts = device.split(':');
29 let major = dev_parts.next().and_then(|s| s.parse::<u64>().ok());
30 let minor = dev_parts.next().and_then(|s| s.parse::<u64>().ok());
31 if major.is_none() || minor.is_none() || dev_parts.next().is_some() {
32 return Err(NucleusError::InvalidResourceLimit(format!(
33 "Invalid device format '{}', expected 'major:minor'",
34 device
35 )));
36 }
37
38 let mut limit = Self {
39 device: device.to_string(),
40 riops: None,
41 wiops: None,
42 rbps: None,
43 wbps: None,
44 };
45
46 for param in parts {
47 let (key, value) = param.split_once('=').ok_or_else(|| {
48 NucleusError::InvalidResourceLimit(format!(
49 "Invalid I/O param '{}', expected key=value",
50 param
51 ))
52 })?;
53 let value: u64 = value.parse().map_err(|_| {
54 NucleusError::InvalidResourceLimit(format!("Invalid I/O value: {}", value))
55 })?;
56
57 match key {
58 "riops" => limit.riops = Some(value),
59 "wiops" => limit.wiops = Some(value),
60 "rbps" => limit.rbps = Some(value),
61 "wbps" => limit.wbps = Some(value),
62 _ => {
63 return Err(NucleusError::InvalidResourceLimit(format!(
64 "Unknown I/O param '{}'",
65 key
66 )));
67 }
68 }
69 }
70
71 Ok(limit)
72 }
73
74 pub fn to_io_max_line(&self) -> String {
76 let mut parts = vec![self.device.clone()];
77 if let Some(v) = self.riops {
78 parts.push(format!("riops={}", v));
79 }
80 if let Some(v) = self.wiops {
81 parts.push(format!("wiops={}", v));
82 }
83 if let Some(v) = self.rbps {
84 parts.push(format!("rbps={}", v));
85 }
86 if let Some(v) = self.wbps {
87 parts.push(format!("wbps={}", v));
88 }
89 parts.join(" ")
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct ResourceLimits {
96 pub memory_bytes: Option<u64>,
98 pub memory_high: Option<u64>,
100 pub memory_swap_max: Option<u64>,
102 pub cpu_quota_us: Option<u64>,
104 pub cpu_period_us: u64,
106 pub cpu_weight: Option<u64>,
108 pub pids_max: Option<u64>,
110 pub io_limits: Vec<IoDeviceLimit>,
112}
113
114impl ResourceLimits {
115 pub fn unlimited() -> Self {
117 Self {
118 memory_bytes: None,
119 memory_high: None,
120 memory_swap_max: None,
121 cpu_quota_us: None,
122 cpu_period_us: 100_000, cpu_weight: None,
124 pids_max: None,
125 io_limits: Vec::new(),
126 }
127 }
128
129 pub fn parse_memory(s: &str) -> Result<u64> {
131 let s = s.trim();
132 if s.is_empty() {
133 return Err(NucleusError::InvalidResourceLimit(
134 "Empty memory limit".to_string(),
135 ));
136 }
137
138 let (num_str, multiplier) = if s.ends_with('K') || s.ends_with('k') {
139 (&s[..s.len() - 1], 1024u64)
140 } else if s.ends_with('M') || s.ends_with('m') {
141 (&s[..s.len() - 1], 1024 * 1024)
142 } else if s.ends_with('G') || s.ends_with('g') {
143 (&s[..s.len() - 1], 1024 * 1024 * 1024)
144 } else if s.ends_with('T') || s.ends_with('t') {
145 (&s[..s.len() - 1], 1024 * 1024 * 1024 * 1024)
146 } else {
147 (s, 1)
149 };
150
151 let num: u64 = num_str.parse().map_err(|_| {
152 NucleusError::InvalidResourceLimit(format!("Invalid memory value: {}", s))
153 })?;
154
155 num.checked_mul(multiplier).ok_or_else(|| {
156 NucleusError::InvalidResourceLimit(format!("Memory value overflows u64: {}", s))
157 })
158 }
159
160 pub fn with_memory(mut self, limit: &str) -> Result<Self> {
165 let bytes = Self::parse_memory(limit)?;
166 self.memory_bytes = Some(bytes);
167 self.memory_high = Some(bytes - bytes / 10);
169 if self.memory_swap_max.is_none() {
171 self.memory_swap_max = Some(0);
172 }
173 Ok(self)
174 }
175
176 pub fn with_swap_enabled(mut self) -> Self {
178 self.memory_swap_max = None;
179 self
180 }
181
182 pub fn with_cpu_cores(mut self, cores: f64) -> Result<Self> {
184 const MAX_CPU_CORES: f64 = 65_536.0;
185
186 if cores <= 0.0 || cores.is_nan() || cores.is_infinite() {
187 return Err(NucleusError::InvalidResourceLimit(
188 "CPU cores must be a finite positive number".to_string(),
189 ));
190 }
191 if cores > MAX_CPU_CORES {
192 return Err(NucleusError::InvalidResourceLimit(format!(
193 "CPU cores must be <= {}",
194 MAX_CPU_CORES
195 )));
196 }
197 let quota = (cores * self.cpu_period_us as f64) as u64;
199 self.cpu_quota_us = Some(quota);
200 Ok(self)
201 }
202
203 pub fn with_pids(mut self, max_pids: u64) -> Result<Self> {
205 if max_pids == 0 {
206 return Err(NucleusError::InvalidResourceLimit(
207 "Max PIDs must be positive".to_string(),
208 ));
209 }
210 self.pids_max = Some(max_pids);
211 Ok(self)
212 }
213
214 pub fn with_cpu_weight(mut self, weight: u64) -> Result<Self> {
216 if !(1..=10000).contains(&weight) {
217 return Err(NucleusError::InvalidResourceLimit(
218 "CPU weight must be between 1 and 10000".to_string(),
219 ));
220 }
221 self.cpu_weight = Some(weight);
222 Ok(self)
223 }
224
225 pub fn with_io_limit(mut self, limit: IoDeviceLimit) -> Self {
227 self.io_limits.push(limit);
228 self
229 }
230}
231
232impl Default for ResourceLimits {
233 fn default() -> Self {
234 Self {
235 pids_max: Some(512),
236 ..Self::unlimited()
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_parse_memory() {
247 assert_eq!(ResourceLimits::parse_memory("1024").unwrap(), 1024);
248 assert_eq!(ResourceLimits::parse_memory("512K").unwrap(), 512 * 1024);
249 assert_eq!(
250 ResourceLimits::parse_memory("512M").unwrap(),
251 512 * 1024 * 1024
252 );
253 assert_eq!(
254 ResourceLimits::parse_memory("2G").unwrap(),
255 2 * 1024 * 1024 * 1024
256 );
257 }
258
259 #[test]
260 fn test_parse_memory_invalid() {
261 assert!(ResourceLimits::parse_memory("").is_err());
262 assert!(ResourceLimits::parse_memory("abc").is_err());
263 assert!(ResourceLimits::parse_memory("M").is_err());
264 }
265
266 #[test]
267 fn test_parse_memory_overflow_rejected() {
268 assert!(ResourceLimits::parse_memory("99999999999999T").is_err());
270 assert!(ResourceLimits::parse_memory("16383P").is_err()); }
273
274 #[test]
275 fn test_with_cpu_cores() {
276 let limits = ResourceLimits::unlimited();
277 let limits = limits.with_cpu_cores(2.0).unwrap();
278 assert_eq!(limits.cpu_quota_us, Some(200_000)); }
280
281 #[test]
282 fn test_with_cpu_cores_fractional() {
283 let limits = ResourceLimits::unlimited();
284 let limits = limits.with_cpu_cores(0.5).unwrap();
285 assert_eq!(limits.cpu_quota_us, Some(50_000)); }
287
288 #[test]
289 fn test_with_cpu_cores_invalid() {
290 let limits = ResourceLimits::unlimited();
291 assert!(limits.with_cpu_cores(0.0).is_err());
292 assert!(ResourceLimits::unlimited().with_cpu_cores(-1.0).is_err());
293 }
294
295 #[test]
296 fn test_with_memory_auto_sets_memory_high() {
297 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
298 let expected_bytes = 1024 * 1024 * 1024u64;
299 assert_eq!(limits.memory_bytes, Some(expected_bytes));
300 assert_eq!(
302 limits.memory_high,
303 Some(expected_bytes - expected_bytes / 10)
304 );
305 }
306
307 #[test]
308 fn test_with_memory_disables_swap_by_default() {
309 let limits = ResourceLimits::unlimited().with_memory("512M").unwrap();
310 assert_eq!(limits.memory_swap_max, Some(0));
311 }
312
313 #[test]
314 fn test_swap_enabled_clears_swap_limit() {
315 let limits = ResourceLimits::unlimited()
316 .with_memory("512M")
317 .unwrap()
318 .with_swap_enabled();
319 assert!(limits.memory_swap_max.is_none());
320 }
321
322 #[test]
323 fn test_with_cpu_weight_valid() {
324 let limits = ResourceLimits::unlimited().with_cpu_weight(100).unwrap();
325 assert_eq!(limits.cpu_weight, Some(100));
326
327 let limits = ResourceLimits::unlimited().with_cpu_weight(1).unwrap();
328 assert_eq!(limits.cpu_weight, Some(1));
329
330 let limits = ResourceLimits::unlimited().with_cpu_weight(10000).unwrap();
331 assert_eq!(limits.cpu_weight, Some(10000));
332 }
333
334 #[test]
335 fn test_with_cpu_weight_invalid() {
336 assert!(ResourceLimits::unlimited().with_cpu_weight(0).is_err());
337 assert!(ResourceLimits::unlimited().with_cpu_weight(10001).is_err());
338 }
339
340 #[test]
341 fn test_io_device_limit_parse_valid() {
342 let limit = IoDeviceLimit::parse("8:0 riops=1000 wbps=10485760").unwrap();
343 assert_eq!(limit.device, "8:0");
344 assert_eq!(limit.riops, Some(1000));
345 assert_eq!(limit.wbps, Some(10485760));
346 assert!(limit.wiops.is_none());
347 assert!(limit.rbps.is_none());
348 }
349
350 #[test]
351 fn test_io_device_limit_parse_all_params() {
352 let limit = IoDeviceLimit::parse("8:0 riops=100 wiops=200 rbps=300 wbps=400").unwrap();
353 assert_eq!(limit.riops, Some(100));
354 assert_eq!(limit.wiops, Some(200));
355 assert_eq!(limit.rbps, Some(300));
356 assert_eq!(limit.wbps, Some(400));
357 }
358
359 #[test]
360 fn test_io_device_limit_parse_invalid() {
361 assert!(IoDeviceLimit::parse("").is_err());
363 assert!(IoDeviceLimit::parse("bad").is_err());
365 assert!(IoDeviceLimit::parse("8:0:1").is_err());
366 assert!(IoDeviceLimit::parse("8:0 riops").is_err());
368 assert!(IoDeviceLimit::parse("8:0 foo=100").is_err());
370 assert!(IoDeviceLimit::parse("8:0 riops=abc").is_err());
372 }
373
374 #[test]
375 fn test_io_device_limit_to_io_max_line() {
376 let limit = IoDeviceLimit {
377 device: "8:0".to_string(),
378 riops: Some(1000),
379 wiops: None,
380 rbps: None,
381 wbps: Some(10485760),
382 };
383 assert_eq!(limit.to_io_max_line(), "8:0 riops=1000 wbps=10485760");
384 }
385
386 #[test]
387 fn test_unlimited_defaults() {
388 let limits = ResourceLimits::unlimited();
389 assert!(limits.memory_bytes.is_none());
390 assert!(limits.memory_high.is_none());
391 assert!(limits.memory_swap_max.is_none());
392 assert!(limits.cpu_quota_us.is_none());
393 assert!(limits.cpu_weight.is_none());
394 assert!(limits.pids_max.is_none());
395 assert!(limits.io_limits.is_empty());
396 }
397
398 #[test]
399 fn test_memory_high_uses_integer_arithmetic() {
400 let limits = ResourceLimits::unlimited().with_memory("1G").unwrap();
402 let bytes = 1024u64 * 1024 * 1024;
403 let expected_high = bytes - bytes / 10; assert_eq!(
405 limits.memory_high,
406 Some(expected_high),
407 "memory_high must be exactly bytes - bytes/10 (integer arithmetic)"
408 );
409 }
410
411 #[test]
412 fn test_cpu_cores_rejects_extreme_values() {
413 assert!(ResourceLimits::unlimited()
415 .with_cpu_cores(f64::NAN)
416 .is_err());
417 assert!(ResourceLimits::unlimited()
418 .with_cpu_cores(f64::INFINITY)
419 .is_err());
420 assert!(
421 ResourceLimits::unlimited()
422 .with_cpu_cores(100_000.0)
423 .is_err(),
424 "CPU cores > 65536 must be rejected to prevent quota overflow"
425 );
426 }
427}