1use crate::fd_table::FdTableManager;
2use crate::pipe_manager::PipeManager;
3use crate::process_table::{ProcessStatus, ProcessTable};
4use crate::pty::PtyManager;
5use crate::socket_table::{SocketState, SocketTable};
6use agentos_bridge::queue_tracker::{register_limit, QueueGauge, TrackedLimit};
7use std::collections::BTreeMap;
8use std::error::Error;
9use std::fmt;
10use std::sync::Arc;
11use vfs::posix::usage::RootFilesystemResourceLimits;
12
13pub use vfs::posix::usage::{
14 measure_filesystem_usage, FileSystemUsage, DEFAULT_MAX_FILESYSTEM_BYTES,
15 DEFAULT_MAX_INODE_COUNT,
16};
17
18pub const DEFAULT_MAX_PROCESSES: usize = 256;
19pub const DEFAULT_MAX_OPEN_FDS: usize = 256;
20pub const DEFAULT_MAX_PIPES: usize = 128;
21pub const DEFAULT_MAX_PTYS: usize = 128;
22pub const DEFAULT_MAX_SOCKETS: usize = 256;
23pub const DEFAULT_MAX_CONNECTIONS: usize = 256;
24pub const DEFAULT_MAX_SOCKET_BUFFERED_BYTES: usize = 4 * 1024 * 1024;
25pub const DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN: usize = 1_024;
26pub const DEFAULT_BLOCKING_READ_TIMEOUT_MS: u64 = 5_000;
27pub const DEFAULT_MAX_PREAD_BYTES: usize = 64 * 1024 * 1024;
28pub const DEFAULT_MAX_FD_WRITE_BYTES: usize = 64 * 1024 * 1024;
29pub const DEFAULT_MAX_PROCESS_ARGV_BYTES: usize = 1024 * 1024;
30pub const DEFAULT_MAX_PROCESS_ENV_BYTES: usize = 1024 * 1024;
31pub const DEFAULT_MAX_READDIR_ENTRIES: usize = 4_096;
32pub const DEFAULT_MAX_RECURSIVE_FS_DEPTH: usize = 128;
33pub const DEFAULT_MAX_RECURSIVE_FS_ENTRIES: usize = 65_536;
34pub const DEFAULT_VIRTUAL_CPU_COUNT: usize = 1;
35pub const DEFAULT_MAX_WASM_MEMORY_BYTES: u64 = 128 * 1024 * 1024;
36
37#[derive(Debug, Clone, PartialEq, Eq, Default)]
38pub struct ResourceSnapshot {
39 pub running_processes: usize,
40 pub exited_processes: usize,
41 pub fd_tables: usize,
42 pub open_fds: usize,
43 pub pipes: usize,
44 pub pipe_buffered_bytes: usize,
45 pub ptys: usize,
46 pub pty_buffered_input_bytes: usize,
47 pub pty_buffered_output_bytes: usize,
48 pub sockets: usize,
49 pub socket_listeners: usize,
50 pub socket_connections: usize,
51 pub socket_buffered_bytes: usize,
52 pub socket_datagram_queue_len: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ResourceLimits {
57 pub virtual_cpu_count: Option<usize>,
58 pub max_processes: Option<usize>,
59 pub max_open_fds: Option<usize>,
60 pub max_pipes: Option<usize>,
61 pub max_ptys: Option<usize>,
62 pub max_sockets: Option<usize>,
63 pub max_connections: Option<usize>,
64 pub max_socket_buffered_bytes: Option<usize>,
65 pub max_socket_datagram_queue_len: Option<usize>,
66 pub max_filesystem_bytes: Option<u64>,
67 pub max_inode_count: Option<usize>,
68 pub max_blocking_read_ms: Option<u64>,
69 pub max_pread_bytes: Option<usize>,
70 pub max_fd_write_bytes: Option<usize>,
71 pub max_process_argv_bytes: Option<usize>,
72 pub max_process_env_bytes: Option<usize>,
73 pub max_readdir_entries: Option<usize>,
74 pub max_recursive_fs_depth: Option<usize>,
75 pub max_recursive_fs_entries: Option<usize>,
76 pub max_wasm_fuel: Option<u64>,
77 pub max_wasm_memory_bytes: Option<u64>,
78 pub max_wasm_stack_bytes: Option<usize>,
79}
80
81impl Default for ResourceLimits {
82 fn default() -> Self {
83 Self {
84 virtual_cpu_count: Some(DEFAULT_VIRTUAL_CPU_COUNT),
85 max_processes: Some(DEFAULT_MAX_PROCESSES),
86 max_open_fds: Some(DEFAULT_MAX_OPEN_FDS),
87 max_pipes: Some(DEFAULT_MAX_PIPES),
88 max_ptys: Some(DEFAULT_MAX_PTYS),
89 max_sockets: Some(DEFAULT_MAX_SOCKETS),
90 max_connections: Some(DEFAULT_MAX_CONNECTIONS),
91 max_socket_buffered_bytes: Some(DEFAULT_MAX_SOCKET_BUFFERED_BYTES),
92 max_socket_datagram_queue_len: Some(DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN),
93 max_filesystem_bytes: Some(DEFAULT_MAX_FILESYSTEM_BYTES),
94 max_inode_count: Some(DEFAULT_MAX_INODE_COUNT),
95 max_blocking_read_ms: Some(DEFAULT_BLOCKING_READ_TIMEOUT_MS),
96 max_pread_bytes: Some(DEFAULT_MAX_PREAD_BYTES),
97 max_fd_write_bytes: Some(DEFAULT_MAX_FD_WRITE_BYTES),
98 max_process_argv_bytes: Some(DEFAULT_MAX_PROCESS_ARGV_BYTES),
99 max_process_env_bytes: Some(DEFAULT_MAX_PROCESS_ENV_BYTES),
100 max_readdir_entries: Some(DEFAULT_MAX_READDIR_ENTRIES),
101 max_recursive_fs_depth: Some(DEFAULT_MAX_RECURSIVE_FS_DEPTH),
102 max_recursive_fs_entries: Some(DEFAULT_MAX_RECURSIVE_FS_ENTRIES),
103 max_wasm_fuel: None,
104 max_wasm_memory_bytes: Some(DEFAULT_MAX_WASM_MEMORY_BYTES),
107 max_wasm_stack_bytes: None,
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct ResourceError {
114 code: &'static str,
115 message: String,
116}
117
118impl RootFilesystemResourceLimits for ResourceLimits {
119 fn max_filesystem_bytes(&self) -> Option<u64> {
120 self.max_filesystem_bytes
121 }
122
123 fn max_inode_count(&self) -> Option<usize> {
124 self.max_inode_count
125 }
126}
127
128impl ResourceError {
129 pub fn code(&self) -> &'static str {
130 self.code
131 }
132
133 fn exhausted(message: impl Into<String>) -> Self {
134 Self {
135 code: "EAGAIN",
136 message: message.into(),
137 }
138 }
139
140 fn file_table_full(message: impl Into<String>) -> Self {
141 Self {
142 code: "ENFILE",
143 message: message.into(),
144 }
145 }
146
147 fn filesystem_full(message: impl Into<String>) -> Self {
148 Self {
149 code: "ENOSPC",
150 message: message.into(),
151 }
152 }
153
154 fn invalid_input(message: impl Into<String>) -> Self {
155 Self {
156 code: "EINVAL",
157 message: message.into(),
158 }
159 }
160
161 fn out_of_memory(message: impl Into<String>) -> Self {
162 Self {
163 code: "ENOMEM",
164 message: message.into(),
165 }
166 }
167}
168
169impl fmt::Display for ResourceError {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 write!(f, "{}: {}", self.code, self.message)
172 }
173}
174
175impl Error for ResourceError {}
176
177#[derive(Debug, Clone, Default)]
178struct ResourceGauges {
183 processes: Option<Arc<QueueGauge>>,
184 open_fds: Option<Arc<QueueGauge>>,
185 pipes: Option<Arc<QueueGauge>>,
186 ptys: Option<Arc<QueueGauge>>,
187 sockets: Option<Arc<QueueGauge>>,
188 connections: Option<Arc<QueueGauge>>,
189 socket_buffered_bytes: Option<Arc<QueueGauge>>,
190 socket_datagram_queue_len: Option<Arc<QueueGauge>>,
191 filesystem_bytes: Option<Arc<QueueGauge>>,
192 inodes: Option<Arc<QueueGauge>>,
193 recursive_fs_depth: Option<Arc<QueueGauge>>,
194 recursive_fs_entries: Option<Arc<QueueGauge>>,
195}
196
197fn register_resource_gauge(name: TrackedLimit, limit: Option<usize>) -> Option<Arc<QueueGauge>> {
198 limit.map(|capacity| register_limit(name, capacity))
199}
200
201fn register_resource_gauge_u64(name: TrackedLimit, limit: Option<u64>) -> Option<Arc<QueueGauge>> {
202 limit.map(|capacity| register_limit(name, usize_saturating_from_u64(capacity)))
203}
204
205fn usize_saturating_from_u64(value: u64) -> usize {
206 usize::try_from(value).unwrap_or(usize::MAX)
207}
208
209impl ResourceGauges {
210 fn new(limits: &ResourceLimits) -> Self {
211 Self {
212 processes: register_resource_gauge(TrackedLimit::VmProcesses, limits.max_processes),
213 open_fds: register_resource_gauge(TrackedLimit::VmOpenFds, limits.max_open_fds),
214 pipes: register_resource_gauge(TrackedLimit::VmPipes, limits.max_pipes),
215 ptys: register_resource_gauge(TrackedLimit::VmPtys, limits.max_ptys),
216 sockets: register_resource_gauge(TrackedLimit::VmSockets, limits.max_sockets),
217 connections: register_resource_gauge(
218 TrackedLimit::VmConnections,
219 limits.max_connections,
220 ),
221 socket_buffered_bytes: register_resource_gauge(
222 TrackedLimit::VmSocketBufferedBytes,
223 limits.max_socket_buffered_bytes,
224 ),
225 socket_datagram_queue_len: register_resource_gauge(
226 TrackedLimit::VmSocketDatagramQueueLen,
227 limits.max_socket_datagram_queue_len,
228 ),
229 filesystem_bytes: register_resource_gauge_u64(
230 TrackedLimit::VmFilesystemBytes,
231 limits.max_filesystem_bytes,
232 ),
233 inodes: register_resource_gauge(TrackedLimit::VmInodes, limits.max_inode_count),
234 recursive_fs_depth: register_resource_gauge(
235 TrackedLimit::VmRecursiveFsDepth,
236 limits.max_recursive_fs_depth,
237 ),
238 recursive_fs_entries: register_resource_gauge(
239 TrackedLimit::VmRecursiveFsEntries,
240 limits.max_recursive_fs_entries,
241 ),
242 }
243 }
244}
245
246pub struct ResourceAccountant {
247 limits: ResourceLimits,
248 gauges: ResourceGauges,
249}
250
251impl ResourceAccountant {
252 pub fn new(limits: ResourceLimits) -> Self {
253 let gauges = ResourceGauges::new(&limits);
254 Self { limits, gauges }
255 }
256
257 fn observe_resource_gauges(&self, snapshot: &ResourceSnapshot) {
260 if let Some(gauge) = &self.gauges.processes {
261 gauge.observe_depth(snapshot.running_processes + snapshot.exited_processes);
262 }
263 if let Some(gauge) = &self.gauges.open_fds {
264 gauge.observe_depth(snapshot.open_fds);
265 }
266 if let Some(gauge) = &self.gauges.pipes {
267 gauge.observe_depth(snapshot.pipes);
268 }
269 if let Some(gauge) = &self.gauges.ptys {
270 gauge.observe_depth(snapshot.ptys);
271 }
272 if let Some(gauge) = &self.gauges.sockets {
273 gauge.observe_depth(snapshot.sockets);
274 }
275 if let Some(gauge) = &self.gauges.connections {
276 gauge.observe_depth(snapshot.socket_connections);
277 }
278 if let Some(gauge) = &self.gauges.socket_buffered_bytes {
279 gauge.observe_depth(snapshot.socket_buffered_bytes);
280 }
281 if let Some(gauge) = &self.gauges.socket_datagram_queue_len {
282 gauge.observe_depth(snapshot.socket_datagram_queue_len);
283 }
284 }
285
286 pub fn limits(&self) -> &ResourceLimits {
287 &self.limits
288 }
289
290 pub fn snapshot(
291 &self,
292 processes: &ProcessTable,
293 fd_tables: &FdTableManager,
294 pipes: &PipeManager,
295 ptys: &PtyManager,
296 sockets: &SocketTable,
297 ) -> ResourceSnapshot {
298 let process_list = processes.list_processes();
299 let running_processes = process_list
300 .values()
301 .filter(|process| process.status == ProcessStatus::Running)
302 .count();
303 let exited_processes = process_list
304 .values()
305 .filter(|process| process.status == ProcessStatus::Exited)
306 .count();
307 let socket_snapshot = sockets.snapshot();
308
309 let snapshot = ResourceSnapshot {
310 running_processes,
311 exited_processes,
312 fd_tables: fd_tables.len(),
313 open_fds: fd_tables.total_open_fds(),
314 pipes: pipes.pipe_count(),
315 pipe_buffered_bytes: pipes.buffered_bytes(),
316 ptys: ptys.pty_count(),
317 pty_buffered_input_bytes: ptys.buffered_input_bytes(),
318 pty_buffered_output_bytes: ptys.buffered_output_bytes(),
319 sockets: socket_snapshot.sockets,
320 socket_listeners: socket_snapshot.listeners,
321 socket_connections: socket_snapshot.connections,
322 socket_buffered_bytes: socket_snapshot.buffered_bytes,
323 socket_datagram_queue_len: socket_snapshot.datagram_queue_len,
324 };
325 self.observe_resource_gauges(&snapshot);
326 snapshot
327 }
328
329 pub fn check_process_spawn(
330 &self,
331 snapshot: &ResourceSnapshot,
332 additional_fds: usize,
333 ) -> Result<(), ResourceError> {
334 if let Some(limit) = self.limits.max_processes {
335 if snapshot.running_processes + snapshot.exited_processes >= limit {
336 return Err(ResourceError::exhausted("maximum process limit reached"));
337 }
338 }
339
340 self.check_open_fds(snapshot, additional_fds)
341 }
342
343 pub fn check_process_argv_bytes(
344 &self,
345 command: &str,
346 args: &[String],
347 ) -> Result<(), ResourceError> {
348 if let Some(limit) = self.limits.max_process_argv_bytes {
349 let total = argv_payload_bytes(command, args);
350 if total > limit {
351 return Err(ResourceError::invalid_input(format!(
352 "process argv payload {total} bytes exceeds configured limit {limit}"
353 )));
354 }
355 }
356
357 Ok(())
358 }
359
360 pub fn check_process_env_bytes(
361 &self,
362 inherited_env: &BTreeMap<String, String>,
363 overrides: &BTreeMap<String, String>,
364 ) -> Result<(), ResourceError> {
365 if let Some(limit) = self.limits.max_process_env_bytes {
366 let total = merged_env_payload_bytes(inherited_env, overrides);
367 if total > limit {
368 return Err(ResourceError::invalid_input(format!(
369 "process environment payload {total} bytes exceeds configured limit {limit}"
370 )));
371 }
372 }
373
374 Ok(())
375 }
376
377 pub fn check_pipe_allocation(&self, snapshot: &ResourceSnapshot) -> Result<(), ResourceError> {
378 if let Some(limit) = self.limits.max_pipes {
379 if snapshot.pipes >= limit {
380 return Err(ResourceError::exhausted("maximum pipe count reached"));
381 }
382 }
383
384 self.check_open_fds(snapshot, 2)
385 }
386
387 pub fn check_pty_allocation(&self, snapshot: &ResourceSnapshot) -> Result<(), ResourceError> {
388 if let Some(limit) = self.limits.max_ptys {
389 if snapshot.ptys >= limit {
390 return Err(ResourceError::exhausted("maximum PTY count reached"));
391 }
392 }
393
394 self.check_open_fds(snapshot, 2)
395 }
396
397 pub fn check_socket_allocation(
398 &self,
399 snapshot: &ResourceSnapshot,
400 ) -> Result<(), ResourceError> {
401 if let Some(limit) = self.limits.max_sockets {
402 if snapshot.sockets >= limit {
403 return Err(ResourceError::exhausted("maximum socket count reached"));
404 }
405 }
406
407 Ok(())
408 }
409
410 pub fn check_socket_state_transition(
411 &self,
412 snapshot: &ResourceSnapshot,
413 current: SocketState,
414 next: SocketState,
415 ) -> Result<(), ResourceError> {
416 if !current.counts_as_connection() && next.counts_as_connection() {
417 if let Some(limit) = self.limits.max_connections {
418 if snapshot.socket_connections >= limit {
419 return Err(ResourceError::exhausted("maximum connection count reached"));
420 }
421 }
422 }
423
424 Ok(())
425 }
426
427 pub fn check_socket_buffer_growth(
428 &self,
429 snapshot: &ResourceSnapshot,
430 additional_bytes: usize,
431 ) -> Result<(), ResourceError> {
432 if let Some(limit) = self.limits.max_socket_buffered_bytes {
433 if snapshot
434 .socket_buffered_bytes
435 .saturating_add(additional_bytes)
436 > limit
437 {
438 return Err(ResourceError::exhausted(
439 "maximum socket buffered byte limit reached",
440 ));
441 }
442 }
443
444 Ok(())
445 }
446
447 pub fn check_socket_datagram_enqueue(
448 &self,
449 snapshot: &ResourceSnapshot,
450 additional_bytes: usize,
451 ) -> Result<(), ResourceError> {
452 self.check_socket_buffer_growth(snapshot, additional_bytes)?;
453 if let Some(limit) = self.limits.max_socket_datagram_queue_len {
454 if snapshot.socket_datagram_queue_len.saturating_add(1) > limit {
455 return Err(ResourceError::exhausted(
456 "maximum socket datagram queue length reached",
457 ));
458 }
459 }
460
461 Ok(())
462 }
463
464 pub fn check_pread_length(&self, length: usize) -> Result<(), ResourceError> {
465 if let Some(limit) = self.limits.max_pread_bytes {
466 if length > limit {
467 return Err(ResourceError::invalid_input(format!(
468 "pread length {length} exceeds configured limit {limit}"
469 )));
470 }
471 }
472
473 Ok(())
474 }
475
476 pub fn check_fd_write_size(&self, size: usize) -> Result<(), ResourceError> {
477 if let Some(limit) = self.limits.max_fd_write_bytes {
478 if size > limit {
479 return Err(ResourceError::invalid_input(format!(
480 "write size {size} exceeds configured limit {limit}"
481 )));
482 }
483 }
484
485 Ok(())
486 }
487
488 pub fn check_fd_allocation(
489 &self,
490 snapshot: &ResourceSnapshot,
491 additional_fds: usize,
492 ) -> Result<(), ResourceError> {
493 self.check_open_fds(snapshot, additional_fds)
494 }
495
496 pub fn max_readdir_entries(&self) -> Option<usize> {
497 self.limits.max_readdir_entries
498 }
499
500 pub fn max_recursive_fs_depth(&self) -> Option<usize> {
501 self.limits.max_recursive_fs_depth
502 }
503
504 pub fn max_recursive_fs_entries(&self) -> Option<usize> {
505 self.limits.max_recursive_fs_entries
506 }
507
508 pub fn check_readdir_entries(&self, entries: usize) -> Result<(), ResourceError> {
509 if let Some(limit) = self.limits.max_readdir_entries {
510 if entries > limit {
511 return Err(ResourceError::out_of_memory(format!(
512 "directory listing with {entries} entries exceeds configured limit {limit}"
513 )));
514 }
515 }
516
517 Ok(())
518 }
519
520 pub fn check_recursive_fs_depth(&self, depth: usize) -> Result<(), ResourceError> {
521 if let Some(gauge) = self.gauges.recursive_fs_depth.as_ref() {
522 gauge.observe_depth(depth);
523 }
524 if let Some(limit) = self.limits.max_recursive_fs_depth {
525 if depth > limit {
526 return Err(ResourceError::out_of_memory(format!(
527 "recursive filesystem operation depth {depth} exceeds configured limit {limit}"
528 )));
529 }
530 }
531
532 Ok(())
533 }
534
535 pub fn check_recursive_fs_entries(&self, entries: usize) -> Result<(), ResourceError> {
536 if let Some(gauge) = self.gauges.recursive_fs_entries.as_ref() {
537 gauge.observe_depth(entries);
538 }
539 if let Some(limit) = self.limits.max_recursive_fs_entries {
540 if entries > limit {
541 return Err(ResourceError::out_of_memory(format!(
542 "recursive filesystem operation with {entries} entries exceeds configured limit {limit}"
543 )));
544 }
545 }
546
547 Ok(())
548 }
549
550 fn check_open_fds(
551 &self,
552 snapshot: &ResourceSnapshot,
553 additional_fds: usize,
554 ) -> Result<(), ResourceError> {
555 if let Some(limit) = self.limits.max_open_fds {
556 if snapshot.open_fds.saturating_add(additional_fds) > limit {
557 return Err(ResourceError::file_table_full(
558 "maximum open file descriptor limit reached",
559 ));
560 }
561 }
562
563 Ok(())
564 }
565
566 pub fn check_filesystem_usage(
567 &self,
568 _usage: &FileSystemUsage,
569 resulting_bytes: u64,
570 resulting_inodes: usize,
571 ) -> Result<(), ResourceError> {
572 if let Some(limit) = self.limits.max_filesystem_bytes {
573 if resulting_bytes > limit {
574 return Err(ResourceError::filesystem_full(
575 "maximum filesystem size limit reached",
576 ));
577 }
578 }
579
580 if let Some(limit) = self.limits.max_inode_count {
581 if resulting_inodes > limit {
582 return Err(ResourceError::filesystem_full(
583 "maximum inode count limit reached",
584 ));
585 }
586 }
587
588 if let Some(gauge) = &self.gauges.filesystem_bytes {
592 gauge.observe_depth(usize_saturating_from_u64(resulting_bytes));
593 }
594 if let Some(gauge) = &self.gauges.inodes {
595 gauge.observe_depth(resulting_inodes);
596 }
597 Ok(())
598 }
599}
600
601fn argv_payload_bytes(command: &str, args: &[String]) -> usize {
602 let command_bytes = command.len().saturating_add(1);
603 command_bytes.saturating_add(
604 args.iter()
605 .map(|arg| arg.len().saturating_add(1))
606 .sum::<usize>(),
607 )
608}
609
610fn env_entry_payload_bytes(key: &str, value: &str) -> usize {
611 key.len()
612 .saturating_add(1)
613 .saturating_add(value.len())
614 .saturating_add(1)
615}
616
617fn merged_env_payload_bytes(
618 inherited_env: &BTreeMap<String, String>,
619 overrides: &BTreeMap<String, String>,
620) -> usize {
621 let mut total = inherited_env
622 .iter()
623 .map(|(key, value)| env_entry_payload_bytes(key, value))
624 .sum::<usize>();
625
626 for (key, value) in overrides {
627 if let Some(previous) = inherited_env.get(key) {
628 total = total.saturating_sub(env_entry_payload_bytes(key, previous));
629 }
630 total = total.saturating_add(env_entry_payload_bytes(key, value));
631 }
632
633 total
634}
635
636#[cfg(test)]
637mod gauge_tests {
638 use super::*;
639 use agentos_bridge::queue_tracker::{set_limit_warning_handler, LimitWarning, TrackedLimit};
640 use std::sync::{Arc, Mutex};
641
642 #[test]
643 fn resource_gauges_track_usage_and_warn_on_approach() {
644 let captured: Arc<Mutex<Vec<LimitWarning>>> = Arc::new(Mutex::new(Vec::new()));
645 let sink = Arc::clone(&captured);
646 set_limit_warning_handler(Box::new(move |warning| {
648 if warning.name == TrackedLimit::VmOpenFds {
649 sink.lock().expect("sink mutex").push(warning.clone());
650 }
651 }));
652
653 let limits = ResourceLimits {
654 max_open_fds: Some(10),
655 ..ResourceLimits::default()
656 };
657 let accountant = ResourceAccountant::new(limits);
658 let snapshot = ResourceSnapshot {
659 open_fds: 9, ..ResourceSnapshot::default()
661 };
662 accountant.observe_resource_gauges(&snapshot);
663
664 let gauge = accountant
666 .gauges
667 .open_fds
668 .as_ref()
669 .expect("open_fds gauge registered when the limit is set");
670 assert_eq!(gauge.depth(), 9);
671 assert_eq!(gauge.capacity(), 10);
672 assert_eq!(gauge.high_water(), 9);
673
674 assert!(
676 captured
677 .lock()
678 .unwrap()
679 .iter()
680 .any(|warning| warning.name == TrackedLimit::VmOpenFds),
681 "open_fds at 90% of cap must emit an approach warning"
682 );
683 }
684
685 #[test]
686 fn unset_limit_registers_no_gauge() {
687 let limits = ResourceLimits {
688 max_ptys: None,
689 ..ResourceLimits::default()
690 };
691 let accountant = ResourceAccountant::new(limits);
692 assert!(
693 accountant.gauges.ptys.is_none(),
694 "an unbounded (None) limit must not register a gauge"
695 );
696 }
697
698 #[test]
699 fn filesystem_gauge_not_latched_by_rejected_write() {
700 let limits = ResourceLimits {
701 max_filesystem_bytes: Some(1000),
702 max_inode_count: Some(100),
703 ..ResourceLimits::default()
704 };
705 let accountant = ResourceAccountant::new(limits);
706 let usage = FileSystemUsage::default();
707
708 let rejected = accountant.check_filesystem_usage(&usage, 2000, 0);
711 assert!(rejected.is_err());
712 let bytes_gauge = accountant
713 .gauges
714 .filesystem_bytes
715 .as_ref()
716 .expect("filesystem_bytes gauge registered");
717 assert_eq!(
718 bytes_gauge.depth(),
719 0,
720 "a rejected over-limit write must not bump the gauge"
721 );
722
723 accountant
725 .check_filesystem_usage(&usage, 500, 7)
726 .expect("under-limit write is accepted");
727 assert_eq!(bytes_gauge.depth(), 500);
728 assert_eq!(
729 accountant.gauges.inodes.as_ref().unwrap().depth(),
730 7,
731 "inode gauge tracks the accepted value"
732 );
733 }
734}