1use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
24use std::sync::mpsc::{Receiver, RecvError, SendError, SyncSender, TrySendError};
25use std::sync::{Arc, Mutex, OnceLock, Weak};
26
27pub const WARN_FILL_PERCENT: usize = 80;
31
32pub const REARM_FILL_PERCENT: usize = 50;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum LimitCategory {
41 Queue,
43 Resource,
45 Memory,
47 Cpu,
49}
50
51impl LimitCategory {
52 pub fn as_str(self) -> &'static str {
54 match self {
55 LimitCategory::Queue => "queue",
56 LimitCategory::Resource => "resource",
57 LimitCategory::Memory => "memory",
58 LimitCategory::Cpu => "cpu",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum TrackedLimit {
69 JavascriptEventChannel,
70 V8SessionFrames,
71 SidecarStdinFrames,
72 SidecarStdoutFrames,
73 CompletedSidecarResponses,
74 PendingProcessEvents,
75 PendingSidecarResponses,
76 OutboundSidecarRequests,
77 VmProcesses,
78 VmOpenFds,
79 VmPipes,
80 VmPtys,
81 VmSockets,
82 VmConnections,
83 VmSocketBufferedBytes,
84 VmSocketDatagramQueueLen,
85 VmFilesystemBytes,
86 VmInodes,
87 VmRecursiveFsDepth,
88 VmRecursiveFsEntries,
89 V8HeapBytes,
90 V8CpuTimeMs,
91 V8WallClockMs,
92 WasmFuelMs,
93 WasmMemoryBytes,
94}
95
96impl TrackedLimit {
97 pub fn as_str(self) -> &'static str {
100 match self {
101 TrackedLimit::JavascriptEventChannel => "javascript_event_channel",
102 TrackedLimit::V8SessionFrames => "v8_session_frames",
103 TrackedLimit::SidecarStdinFrames => "sidecar_stdin_frames",
104 TrackedLimit::SidecarStdoutFrames => "sidecar_stdout_frames",
105 TrackedLimit::CompletedSidecarResponses => "completed_sidecar_responses",
106 TrackedLimit::PendingProcessEvents => "pending_process_events",
107 TrackedLimit::PendingSidecarResponses => "pending_sidecar_responses",
108 TrackedLimit::OutboundSidecarRequests => "outbound_sidecar_requests",
109 TrackedLimit::VmProcesses => "vm_processes",
110 TrackedLimit::VmOpenFds => "vm_open_fds",
111 TrackedLimit::VmPipes => "vm_pipes",
112 TrackedLimit::VmPtys => "vm_ptys",
113 TrackedLimit::VmSockets => "vm_sockets",
114 TrackedLimit::VmConnections => "vm_connections",
115 TrackedLimit::VmSocketBufferedBytes => "vm_socket_buffered_bytes",
116 TrackedLimit::VmSocketDatagramQueueLen => "vm_socket_datagram_queue_len",
117 TrackedLimit::VmFilesystemBytes => "vm_filesystem_bytes",
118 TrackedLimit::VmInodes => "vm_inodes",
119 TrackedLimit::VmRecursiveFsDepth => "vm_recursive_fs_depth",
120 TrackedLimit::VmRecursiveFsEntries => "vm_recursive_fs_entries",
121 TrackedLimit::V8HeapBytes => "v8_heap_bytes",
122 TrackedLimit::V8CpuTimeMs => "v8_cpu_time_ms",
123 TrackedLimit::V8WallClockMs => "v8_wall_clock_ms",
124 TrackedLimit::WasmFuelMs => "wasm_fuel_ms",
125 TrackedLimit::WasmMemoryBytes => "wasm_memory_bytes",
126 }
127 }
128
129 pub fn category(self) -> LimitCategory {
130 match self {
131 TrackedLimit::JavascriptEventChannel
132 | TrackedLimit::V8SessionFrames
133 | TrackedLimit::SidecarStdinFrames
134 | TrackedLimit::SidecarStdoutFrames
135 | TrackedLimit::CompletedSidecarResponses
136 | TrackedLimit::PendingProcessEvents
137 | TrackedLimit::PendingSidecarResponses
138 | TrackedLimit::OutboundSidecarRequests => LimitCategory::Queue,
139 TrackedLimit::VmProcesses
140 | TrackedLimit::VmOpenFds
141 | TrackedLimit::VmPipes
142 | TrackedLimit::VmPtys
143 | TrackedLimit::VmSockets
144 | TrackedLimit::VmConnections
145 | TrackedLimit::VmSocketBufferedBytes
146 | TrackedLimit::VmSocketDatagramQueueLen
147 | TrackedLimit::VmFilesystemBytes
148 | TrackedLimit::VmInodes
149 | TrackedLimit::VmRecursiveFsDepth
150 | TrackedLimit::VmRecursiveFsEntries => LimitCategory::Resource,
151 TrackedLimit::V8HeapBytes | TrackedLimit::WasmMemoryBytes => LimitCategory::Memory,
152 TrackedLimit::V8CpuTimeMs | TrackedLimit::V8WallClockMs | TrackedLimit::WasmFuelMs => {
153 LimitCategory::Cpu
154 }
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
163pub struct LimitWarning {
164 pub name: TrackedLimit,
165 pub category: LimitCategory,
166 pub observed: usize,
167 pub capacity: usize,
168 pub fill_percent: usize,
169}
170
171type LimitWarningHandler = Arc<dyn Fn(&LimitWarning) + Send + Sync>;
172
173fn warning_handler_slot() -> &'static Mutex<Option<LimitWarningHandler>> {
174 static HANDLER: OnceLock<Mutex<Option<LimitWarningHandler>>> = OnceLock::new();
175 HANDLER.get_or_init(|| Mutex::new(None))
176}
177
178pub fn set_limit_warning_handler(handler: Box<dyn Fn(&LimitWarning) + Send + Sync>) {
184 if let Ok(mut slot) = warning_handler_slot().lock() {
185 *slot = Some(Arc::from(handler));
186 }
187}
188
189fn dispatch_warning(warning: &LimitWarning) {
190 let handler = match warning_handler_slot().lock() {
196 Ok(slot) => slot.as_ref().cloned(),
197 Err(_) => None,
198 };
199 if let Some(handler) = handler {
200 handler(warning);
201 }
202}
203
204pub fn warn_limit_exhausted(name: TrackedLimit, observed: usize, capacity: usize) {
208 let fill_percent = observed
209 .saturating_mul(100)
210 .checked_div(capacity)
211 .unwrap_or(0);
212 let category = name.category();
213 tracing::warn!(
214 limit = name.as_str(),
215 category = category.as_str(),
216 observed,
217 capacity,
218 fill_percent,
219 "bounded limit exhausted"
220 );
221 dispatch_warning(&LimitWarning {
222 name,
223 category,
224 observed,
225 capacity,
226 fill_percent,
227 });
228}
229
230#[derive(Debug)]
235pub struct QueueGauge {
236 name: TrackedLimit,
237 category: LimitCategory,
238 capacity: usize,
239 depth: AtomicUsize,
240 high_water: AtomicUsize,
241 warned: AtomicBool,
242}
243
244impl QueueGauge {
245 fn new(name: TrackedLimit, capacity: usize, category: LimitCategory) -> Self {
246 Self {
247 name,
248 category,
249 capacity,
250 depth: AtomicUsize::new(0),
251 high_water: AtomicUsize::new(0),
252 warned: AtomicBool::new(false),
253 }
254 }
255
256 pub fn name(&self) -> TrackedLimit {
258 self.name
259 }
260
261 pub fn category(&self) -> LimitCategory {
263 self.category
264 }
265
266 pub fn capacity(&self) -> usize {
268 self.capacity
269 }
270
271 pub fn depth(&self) -> usize {
273 self.depth.load(Ordering::Acquire)
274 }
275
276 pub fn high_water(&self) -> usize {
278 self.high_water.load(Ordering::Acquire)
279 }
280
281 fn fill_percent(&self, depth: usize) -> usize {
284 depth
285 .saturating_mul(100)
286 .checked_div(self.capacity)
287 .unwrap_or(0)
288 }
289
290 fn evaluate(&self, depth: usize) {
293 self.high_water.fetch_max(depth, Ordering::AcqRel);
294 if self.capacity == 0 {
295 return;
296 }
297 let percent = self.fill_percent(depth);
298 if percent >= WARN_FILL_PERCENT {
299 if !self.warned.swap(true, Ordering::AcqRel) {
300 tracing::warn!(
301 limit = self.name.as_str(),
302 category = self.category.as_str(),
303 observed = depth,
304 capacity = self.capacity,
305 fill_percent = percent,
306 "bounded limit near capacity"
307 );
308 dispatch_warning(&LimitWarning {
312 name: self.name,
313 category: self.category,
314 observed: depth,
315 capacity: self.capacity,
316 fill_percent: percent,
317 });
318 }
319 } else if percent <= REARM_FILL_PERCENT && self.warned.swap(false, Ordering::AcqRel) {
320 tracing::debug!(
321 limit = self.name.as_str(),
322 category = self.category.as_str(),
323 depth,
324 capacity = self.capacity,
325 fill_percent = percent,
326 "bounded limit drained back below threshold"
327 );
328 }
329 }
330
331 pub fn observe_depth(&self, depth: usize) {
334 self.depth.store(depth, Ordering::Release);
335 self.evaluate(depth);
336 }
337
338 pub fn record_enqueue(&self) {
340 let depth = self.depth.fetch_add(1, Ordering::AcqRel) + 1;
341 self.evaluate(depth);
342 }
343
344 pub fn record_dequeue(&self) {
349 let mut current = self.depth.load(Ordering::Acquire);
350 loop {
351 if current == 0 {
352 return;
353 }
354 match self.depth.compare_exchange_weak(
355 current,
356 current - 1,
357 Ordering::AcqRel,
358 Ordering::Acquire,
359 ) {
360 Ok(_) => {
361 self.evaluate(current - 1);
362 break;
363 }
364 Err(actual) => current = actual,
365 }
366 }
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct QueueSnapshot {
373 pub name: TrackedLimit,
374 pub category: LimitCategory,
375 pub depth: usize,
376 pub high_water: usize,
377 pub capacity: usize,
378 pub fill_percent: usize,
379}
380
381#[derive(Default)]
383pub struct QueueRegistry {
384 gauges: Mutex<Vec<Weak<QueueGauge>>>,
385}
386
387impl QueueRegistry {
388 pub fn global() -> &'static QueueRegistry {
391 static REGISTRY: OnceLock<QueueRegistry> = OnceLock::new();
392 REGISTRY.get_or_init(QueueRegistry::default)
393 }
394
395 pub fn register(&self, name: TrackedLimit, capacity: usize) -> Arc<QueueGauge> {
398 let category = name.category();
399 let gauge = Arc::new(QueueGauge::new(name, capacity, category));
400 let mut gauges = self.gauges.lock().expect("queue registry mutex poisoned");
401 gauges.retain(|weak| weak.strong_count() > 0);
402 gauges.push(Arc::downgrade(&gauge));
403 gauge
404 }
405
406 pub fn snapshot(&self) -> Vec<QueueSnapshot> {
408 let mut gauges = self.gauges.lock().expect("queue registry mutex poisoned");
409 gauges.retain(|weak| weak.strong_count() > 0);
410 gauges
411 .iter()
412 .filter_map(Weak::upgrade)
413 .map(|gauge| {
414 let depth = gauge.depth();
415 QueueSnapshot {
416 name: gauge.name(),
417 category: gauge.category(),
418 depth,
419 high_water: gauge.high_water(),
420 capacity: gauge.capacity(),
421 fill_percent: gauge.fill_percent(depth),
422 }
423 })
424 .collect()
425 }
426}
427
428pub fn register_queue(name: TrackedLimit, capacity: usize) -> Arc<QueueGauge> {
431 debug_assert_eq!(name.category(), LimitCategory::Queue);
432 QueueRegistry::global().register(name, capacity)
433}
434
435pub fn register_limit(name: TrackedLimit, capacity: usize) -> Arc<QueueGauge> {
439 QueueRegistry::global().register(name, capacity)
440}
441
442pub fn queue_snapshot() -> Vec<QueueSnapshot> {
444 QueueRegistry::global().snapshot()
445}
446
447pub fn log_queue_snapshot() {
450 for stat in queue_snapshot() {
451 tracing::debug!(
452 limit = stat.name.as_str(),
453 category = stat.category.as_str(),
454 depth = stat.depth,
455 high_water = stat.high_water,
456 capacity = stat.capacity,
457 fill_percent = stat.fill_percent,
458 "limit usage"
459 );
460 }
461}
462
463#[derive(Debug)]
470pub struct TrackedSyncSender<T> {
471 inner: SyncSender<T>,
472 gauge: Arc<QueueGauge>,
473}
474
475impl<T> Clone for TrackedSyncSender<T> {
476 fn clone(&self) -> Self {
477 Self {
478 inner: self.inner.clone(),
479 gauge: Arc::clone(&self.gauge),
480 }
481 }
482}
483
484impl<T> TrackedSyncSender<T> {
485 pub fn send(&self, value: T) -> Result<(), SendError<T>> {
488 self.gauge.record_enqueue();
489 self.inner.send(value)
490 }
491
492 pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>> {
495 match self.inner.try_send(value) {
496 Ok(()) => {
497 self.gauge.record_enqueue();
498 Ok(())
499 }
500 Err(error) => Err(error),
501 }
502 }
503
504 pub fn gauge(&self) -> &Arc<QueueGauge> {
506 &self.gauge
507 }
508}
509
510#[derive(Debug)]
513pub struct TrackedReceiver<T> {
514 inner: Receiver<T>,
515 gauge: Arc<QueueGauge>,
516}
517
518impl<T> TrackedReceiver<T> {
519 pub fn recv(&self) -> Result<T, RecvError> {
521 let value = self.inner.recv()?;
522 self.gauge.record_dequeue();
523 Ok(value)
524 }
525}
526
527pub fn tracked_sync_channel<T>(
531 name: TrackedLimit,
532 capacity: usize,
533) -> (TrackedSyncSender<T>, TrackedReceiver<T>) {
534 let (tx, rx) = std::sync::mpsc::sync_channel(capacity);
535 let gauge = register_queue(name, capacity);
536 (
537 TrackedSyncSender {
538 inner: tx,
539 gauge: Arc::clone(&gauge),
540 },
541 TrackedReceiver { inner: rx, gauge },
542 )
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn gauge_tracks_depth_and_high_water() {
551 let gauge = QueueGauge::new(
552 TrackedLimit::JavascriptEventChannel,
553 10,
554 LimitCategory::Queue,
555 );
556 assert_eq!(gauge.depth(), 0);
557 gauge.record_enqueue();
558 gauge.record_enqueue();
559 assert_eq!(gauge.depth(), 2);
560 assert_eq!(gauge.high_water(), 2);
561 gauge.record_dequeue();
562 assert_eq!(gauge.depth(), 1);
563 assert_eq!(gauge.high_water(), 2);
565 gauge.record_dequeue();
567 gauge.record_dequeue();
568 assert_eq!(gauge.depth(), 0);
569 }
570
571 #[test]
572 fn gauge_warn_flag_is_edge_triggered_with_hysteresis() {
573 let gauge = QueueGauge::new(TrackedLimit::V8SessionFrames, 10, LimitCategory::Queue);
574 gauge.observe_depth(7);
576 assert!(!gauge.warned.load(Ordering::Acquire));
577 gauge.observe_depth(8);
579 assert!(gauge.warned.load(Ordering::Acquire));
580 gauge.observe_depth(9);
582 assert!(gauge.warned.load(Ordering::Acquire));
583 gauge.observe_depth(5);
585 assert!(!gauge.warned.load(Ordering::Acquire));
586 }
587
588 #[test]
589 fn gauge_rearms_on_dequeue_drain() {
590 let gauge = QueueGauge::new(TrackedLimit::SidecarStdoutFrames, 10, LimitCategory::Queue);
593 for _ in 0..9 {
594 gauge.record_enqueue(); }
596 assert_eq!(gauge.depth(), 9);
597 assert!(gauge.warned.load(Ordering::Acquire));
598 for _ in 0..6 {
599 gauge.record_dequeue(); }
601 assert_eq!(gauge.depth(), 3);
602 assert!(!gauge.warned.load(Ordering::Acquire));
603 }
604
605 #[test]
606 fn tracked_channel_reports_usage_through_registry() {
607 let (tx, rx) = tracked_sync_channel::<u32>(TrackedLimit::SidecarStdoutFrames, 4);
608 tx.send(1).unwrap();
609 tx.send(2).unwrap();
610
611 let snapshot = queue_snapshot();
612 let entry = snapshot
613 .iter()
614 .find(|stat| stat.name == TrackedLimit::SidecarStdoutFrames)
615 .expect("registered queue should appear in snapshot");
616 assert_eq!(entry.depth, 2);
617 assert_eq!(entry.capacity, 4);
618 assert_eq!(entry.high_water, 2);
619 assert_eq!(entry.fill_percent, 50);
620 assert_eq!(entry.category, LimitCategory::Queue);
621
622 assert_eq!(rx.recv().unwrap(), 1);
623 assert_eq!(tx.gauge().depth(), 1);
624
625 drop(tx);
627 drop(rx);
628 assert!(queue_snapshot()
629 .iter()
630 .all(|stat| stat.name != TrackedLimit::SidecarStdoutFrames));
631 }
632
633 #[test]
634 fn warning_sink_fires_once_per_crossing() {
635 let captured: Arc<Mutex<Vec<LimitWarning>>> = Arc::new(Mutex::new(Vec::new()));
636 let sink = Arc::clone(&captured);
637 set_limit_warning_handler(Box::new(move |warning| {
640 if warning.name == TrackedLimit::VmPipes {
641 sink.lock().expect("sink mutex").push(warning.clone());
642 }
643 }));
644
645 let gauge = register_limit(TrackedLimit::VmPipes, 10);
646 gauge.observe_depth(7); assert!(captured.lock().unwrap().is_empty());
648 gauge.observe_depth(9); gauge.observe_depth(10); let warnings = captured.lock().unwrap();
652 assert_eq!(
653 warnings.len(),
654 1,
655 "warning sink must fire once per crossing"
656 );
657 assert_eq!(warnings[0].category, LimitCategory::Resource);
658 assert_eq!(warnings[0].capacity, 10);
659 assert!(warnings[0].fill_percent >= WARN_FILL_PERCENT);
660 }
661
662 #[test]
663 fn exhausted_warning_sink_fires_immediately() {
664 let captured: Arc<Mutex<Vec<LimitWarning>>> = Arc::new(Mutex::new(Vec::new()));
665 let sink = Arc::clone(&captured);
666 set_limit_warning_handler(Box::new(move |warning| {
667 if warning.name == TrackedLimit::V8CpuTimeMs {
668 sink.lock().expect("sink mutex").push(warning.clone());
669 }
670 }));
671
672 warn_limit_exhausted(TrackedLimit::V8CpuTimeMs, 30_000, 30_000);
673
674 let warnings = captured.lock().unwrap();
675 assert_eq!(warnings.len(), 1);
676 assert_eq!(warnings[0].category, LimitCategory::Cpu);
677 assert_eq!(warnings[0].fill_percent, 100);
678 }
679}