1use crate::types::{CancelKind, CancelReason};
8use parking_lot::Mutex;
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, HashMap, VecDeque};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering};
13use std::time::{Duration, SystemTime};
14
15const DEFAULT_MAX_CANCEL_REASON_BYTES: usize = 64;
20
21const MAX_QUEUE_DEPTH_ENTITIES: usize = 4096;
27
28const QUEUE_DEPTH_OVERFLOW_BUCKET: &str = "__overflow__";
32
33const DEFAULT_MAX_PENDING_PER_WORK_TYPE: usize = 10_000;
37
38fn saturating_system_time_sub(time: SystemTime, duration: Duration) -> SystemTime {
39 time.checked_sub(duration).unwrap_or(SystemTime::UNIX_EPOCH)
40}
41
42fn truncate_to_bytes(s: &str, max_bytes: usize) -> String {
46 if s.len() <= max_bytes {
47 return s.to_string();
48 }
49 let mut end = max_bytes;
51 while end > 0 && !s.is_char_boundary(end) {
52 end -= 1;
53 }
54 let mut out = String::with_capacity(end + 3);
55 out.push_str(&s[..end]);
56 out.push('…');
57 out
58}
59
60#[derive(Debug, Clone)]
62pub struct CancellationDebtConfig {
63 pub max_queue_depth: usize,
65 pub max_pending_duration: Duration,
67 pub rate_sampling_window: Duration,
69 pub min_processing_rate: f64,
71 pub debt_threshold_percentage: f64,
73 pub enable_auto_relief: bool,
75 pub max_tracking_memory_mb: usize,
77 pub max_pending_per_work_type: usize,
81 pub max_cancel_reason_bytes: usize,
85}
86
87impl Default for CancellationDebtConfig {
88 fn default() -> Self {
89 Self {
90 max_queue_depth: 10_000,
91 max_pending_duration: Duration::from_secs(30),
92 rate_sampling_window: Duration::from_secs(60),
93 min_processing_rate: 100.0, debt_threshold_percentage: 75.0, enable_auto_relief: false, max_tracking_memory_mb: 50,
97 max_pending_per_work_type: DEFAULT_MAX_PENDING_PER_WORK_TYPE,
98 max_cancel_reason_bytes: DEFAULT_MAX_CANCEL_REASON_BYTES,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub enum WorkType {
106 TaskCleanup,
108 RegionCleanup,
110 ResourceFinalization,
112 ObligationSettlement,
114 WakerCleanup,
116 ChannelCleanup,
118}
119
120#[derive(Debug, Clone)]
122pub struct PendingWork {
123 pub work_id: u64,
125 pub work_type: WorkType,
127 pub entity_id: String,
129 pub queued_at: SystemTime,
131 pub priority: u32,
133 pub estimated_cost: u32,
135 pub cancel_reason: String,
140 pub cancel_kind: CancelKind,
144 pub dependencies: Vec<u64>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DebtSnapshot {
151 pub snapshot_time: SystemTime,
153 pub total_pending: usize,
155 pub pending_by_type: HashMap<WorkType, usize>,
157 pub debt_percentage: f64,
159 pub processing_rate: f64,
161 pub entity_queue_depths: HashMap<String, usize>,
163 pub oldest_work_age: Duration,
165 pub memory_usage_mb: f64,
167 pub alert_level: DebtAlertLevel,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum DebtAlertLevel {
174 Normal,
176 Watch,
178 Warning,
180 Critical,
182 Emergency,
184}
185
186impl DebtAlertLevel {
187 #[must_use]
190 pub const fn as_u8(self) -> u8 {
191 match self {
192 Self::Normal => 0,
193 Self::Watch => 1,
194 Self::Warning => 2,
195 Self::Critical => 3,
196 Self::Emergency => 4,
197 }
198 }
199
200 #[must_use]
204 pub const fn from_u8(v: u8) -> Self {
205 match v {
206 1 => Self::Watch,
207 2 => Self::Warning,
208 3 => Self::Critical,
209 4 => Self::Emergency,
210 _ => Self::Normal,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct DebtAlert {
218 pub level: DebtAlertLevel,
220 pub message: String,
222 pub work_type: Option<WorkType>,
224 pub entity_id: Option<String>,
226 pub metric_value: f64,
228 pub threshold: f64,
230 pub generated_at: SystemTime,
232 pub remediation_suggestions: Vec<String>,
234}
235
236#[derive(Debug)]
238struct ProcessingStats {
239 items_processed: VecDeque<(SystemTime, usize)>,
241 total_processed: AtomicU64,
243 last_rate: f64,
245 last_rate_time: SystemTime,
247}
248
249impl ProcessingStats {
250 fn new() -> Self {
251 Self {
252 items_processed: VecDeque::new(),
253 total_processed: AtomicU64::new(0),
254 last_rate: 0.0,
255 last_rate_time: super::replayable_system_time(),
256 }
257 }
258
259 fn record_processing(&mut self, count: usize, now: SystemTime, window: Duration) {
260 self.items_processed.push_back((now, count));
261 self.total_processed
262 .fetch_add(count as u64, Ordering::Relaxed);
263
264 let cutoff = saturating_system_time_sub(now, window);
270 while let Some(&(time, _)) = self.items_processed.front() {
271 if time < cutoff {
272 self.items_processed.pop_front();
273 } else {
274 break;
275 }
276 }
277 }
278
279 fn calculate_rate(&mut self, window: Duration, now: SystemTime) -> f64 {
280 if now
282 .duration_since(self.last_rate_time)
283 .unwrap_or(Duration::ZERO)
284 < Duration::from_secs(5)
285 {
286 return self.last_rate;
287 }
288
289 let cutoff = saturating_system_time_sub(now, window);
290 let total_in_window: usize = self
291 .items_processed
292 .iter()
293 .filter(|&&(time, _)| time >= cutoff)
294 .map(|&(_, count)| count)
295 .sum();
296
297 let rate = if window.as_secs() > 0 {
298 total_in_window as f64 / window.as_secs() as f64
299 } else {
300 0.0
301 };
302
303 self.last_rate = rate;
304 self.last_rate_time = now;
305 rate
306 }
307}
308
309pub struct CancellationDebtMonitor {
316 config: CancellationDebtConfig,
317 pending_work: Arc<Mutex<HashMap<WorkType, BTreeMap<u64, PendingWork>>>>,
319 processing_stats: Arc<Mutex<HashMap<WorkType, ProcessingStats>>>,
321 next_work_id: AtomicU64,
323 current_alert_level: AtomicU8,
326 recent_alerts: Arc<Mutex<VecDeque<DebtAlert>>>,
328 memory_usage_bytes: AtomicUsize,
330 eviction_count: AtomicU64,
333 monitoring_loop_panic_count: AtomicU64,
338}
339
340impl CancellationDebtMonitor {
341 #[must_use]
343 pub fn new(config: CancellationDebtConfig) -> Self {
344 Self {
345 config,
346 pending_work: Arc::new(Mutex::new(HashMap::new())),
347 processing_stats: Arc::new(Mutex::new(HashMap::new())),
348 next_work_id: AtomicU64::new(1),
349 current_alert_level: AtomicU8::new(DebtAlertLevel::Normal.as_u8()),
350 recent_alerts: Arc::new(Mutex::new(VecDeque::new())),
351 memory_usage_bytes: AtomicUsize::new(0),
352 eviction_count: AtomicU64::new(0),
353 monitoring_loop_panic_count: AtomicU64::new(0),
354 }
355 }
356
357 #[must_use]
363 pub fn monitoring_loop_panic_count(&self) -> u64 {
364 self.monitoring_loop_panic_count.load(Ordering::Relaxed)
365 }
366
367 pub fn record_monitoring_loop_panic(&self) {
372 self.monitoring_loop_panic_count
373 .fetch_add(1, Ordering::Relaxed);
374 }
375
376 #[must_use]
379 pub fn eviction_count(&self) -> u64 {
380 self.eviction_count.load(Ordering::Relaxed)
381 }
382
383 #[must_use]
385 pub fn default() -> Self {
386 Self::new(CancellationDebtConfig::default())
387 }
388
389 pub fn queue_work(
400 &self,
401 work_type: WorkType,
402 entity_id: String,
403 priority: u32,
404 estimated_cost: u32,
405 cancel_reason: &CancelReason,
406 cancel_kind: CancelKind,
407 dependencies: Vec<u64>,
408 ) -> u64 {
409 let work_id = self.next_work_id.fetch_add(1, Ordering::Relaxed);
410 let now = super::replayable_system_time();
411
412 let cancel_reason_text = truncate_to_bytes(
413 &format!("{cancel_reason}"),
414 self.config.max_cancel_reason_bytes,
415 );
416 let work = PendingWork {
417 work_id,
418 work_type,
419 entity_id,
420 queued_at: now,
421 priority,
422 estimated_cost,
423 cancel_reason: cancel_reason_text,
424 cancel_kind,
425 dependencies,
426 };
427
428 let work_size =
430 std::mem::size_of::<PendingWork>() + work.entity_id.len() + work.cancel_reason.len();
431 self.memory_usage_bytes
432 .fetch_add(work_size, Ordering::Relaxed);
433
434 let evicted_for_alert = {
437 let mut pending = self.pending_work.lock();
438 let map = pending.entry(work_type).or_default();
439 let evicted = if map.len() >= self.config.max_pending_per_work_type {
440 map.pop_first().map(|(_, w)| {
444 let evicted_size = std::mem::size_of::<PendingWork>()
445 + w.entity_id.len()
446 + w.cancel_reason.len();
447 self.memory_usage_bytes
448 .fetch_sub(evicted_size, Ordering::Relaxed);
449 self.eviction_count.fetch_add(1, Ordering::Relaxed);
450 w.work_id
451 })
452 } else {
453 None
454 };
455 map.insert(work_id, work);
456 evicted
457 };
458
459 if let Some(evicted_id) = evicted_for_alert {
460 self.generate_alert(DebtAlert {
461 level: DebtAlertLevel::Emergency,
462 message: format!(
463 "evicted oldest pending {work_type:?} (work_id={evicted_id}) — \
464 per-type cap of {} reached (br-asupersync-i40ap4)",
465 self.config.max_pending_per_work_type
466 ),
467 work_type: Some(work_type),
468 entity_id: None,
469 metric_value: self.config.max_pending_per_work_type as f64,
470 threshold: self.config.max_pending_per_work_type as f64,
471 generated_at: now,
472 remediation_suggestions: vec![
473 "Investigate why pending work is not being completed".to_string(),
474 "Increase max_pending_per_work_type if eviction is benign".to_string(),
475 ],
476 });
477 }
478
479 self.check_debt_levels();
481
482 work_id
483 }
484
485 pub fn complete_work(&self, work_id: u64) -> bool {
487 let now = super::replayable_system_time();
488 let mut found_work = None;
489
490 {
492 let mut pending = self.pending_work.lock();
493 for (work_type, work_map) in pending.iter_mut() {
494 if let Some(work) = work_map.remove(&work_id) {
495 found_work = Some((*work_type, work));
496 break;
497 }
498 }
499 }
500
501 if let Some((work_type, work)) = found_work {
502 let work_size = std::mem::size_of::<PendingWork>()
504 + work.entity_id.len()
505 + work.cancel_reason.len()
506 ;
507 self.memory_usage_bytes
508 .fetch_sub(work_size, Ordering::Relaxed);
509
510 {
512 let mut stats = self.processing_stats.lock();
513 stats
514 .entry(work_type)
515 .or_insert_with(ProcessingStats::new)
516 .record_processing(1, now, self.config.rate_sampling_window);
517 }
518
519 true
520 } else {
521 false
522 }
523 }
524
525 pub fn complete_work_batch(&self, work_ids: &[u64]) -> usize {
527 let now = super::replayable_system_time();
528 let mut completed_count = 0;
529 let mut completed_by_type: HashMap<WorkType, usize> = HashMap::new();
530
531 {
533 let mut pending = self.pending_work.lock();
534 for &work_id in work_ids {
535 for (work_type, work_map) in pending.iter_mut() {
536 if let Some(work) = work_map.remove(&work_id) {
537 completed_count += 1;
538 *completed_by_type.entry(*work_type).or_default() += 1;
539
540 let work_size = std::mem::size_of::<PendingWork>()
542 + work.entity_id.len()
543 + work.cancel_reason.len()
544 ;
545 self.memory_usage_bytes
546 .fetch_sub(work_size, Ordering::Relaxed);
547 break;
548 }
549 }
550 }
551 }
552
553 {
555 let mut stats = self.processing_stats.lock();
556 for (work_type, count) in completed_by_type {
557 stats
558 .entry(work_type)
559 .or_insert_with(ProcessingStats::new)
560 .record_processing(count, now, self.config.rate_sampling_window);
561 }
562 }
563
564 completed_count
565 }
566
567 pub fn get_debt_snapshot(&self) -> DebtSnapshot {
569 let now = super::replayable_system_time();
570 let pending = self.pending_work.lock();
571
572 let mut total_pending = 0;
574 let mut pending_by_type = HashMap::new();
575 let mut entity_queue_depths = HashMap::new();
576 let mut oldest_work_age = Duration::ZERO;
577
578 for (work_type, work_map) in pending.iter() {
579 let type_count = work_map.len();
580 total_pending += type_count;
581 pending_by_type.insert(*work_type, type_count);
582
583 for work in work_map.values() {
584 let key = if entity_queue_depths.contains_key(&work.entity_id)
592 || entity_queue_depths.len() < MAX_QUEUE_DEPTH_ENTITIES
593 {
594 work.entity_id.clone()
595 } else {
596 QUEUE_DEPTH_OVERFLOW_BUCKET.to_string()
597 };
598 *entity_queue_depths.entry(key).or_default() += 1;
599
600 if let Ok(age) = now.duration_since(work.queued_at) {
602 oldest_work_age = oldest_work_age.max(age);
603 }
604 }
605 }
606
607 let debt_percentage = if self.config.max_queue_depth > 0 {
609 (total_pending as f64 / self.config.max_queue_depth as f64) * 100.0
610 } else {
611 0.0
612 };
613
614 let processing_rate = {
616 let mut stats = self.processing_stats.lock();
617 let mut total_rate = 0.0;
618 for stat in stats.values_mut() {
619 total_rate += stat.calculate_rate(self.config.rate_sampling_window, now);
620 }
621 total_rate
622 };
623
624 let memory_usage_mb =
626 self.memory_usage_bytes.load(Ordering::Relaxed) as f64 / (1024.0 * 1024.0);
627
628 let alert_level = DebtAlertLevel::from_u8(self.current_alert_level.load(Ordering::Relaxed));
630
631 DebtSnapshot {
632 snapshot_time: now,
633 total_pending,
634 pending_by_type,
635 debt_percentage,
636 processing_rate,
637 entity_queue_depths,
638 oldest_work_age,
639 memory_usage_mb,
640 alert_level,
641 }
642 }
643
644 pub fn get_entity_pending_work(&self, entity_id: &str) -> Vec<PendingWork> {
646 let pending = self.pending_work.lock();
647 let mut result = Vec::new();
648
649 for work_map in pending.values() {
650 for work in work_map.values() {
651 if work.entity_id == entity_id {
652 result.push(work.clone());
653 }
654 }
655 }
656
657 result.sort_by(|a, b| b.priority.cmp(&a.priority));
658 result
659 }
660
661 pub fn get_priority_work(&self, limit: usize) -> Vec<PendingWork> {
663 let pending = self.pending_work.lock();
664 let mut result = Vec::new();
665
666 for work_map in pending.values() {
667 for work in work_map.values() {
668 result.push(work.clone());
669 }
670 }
671
672 result.sort_by(|a, b| {
673 match b.priority.cmp(&a.priority) {
675 std::cmp::Ordering::Equal => b.queued_at.cmp(&a.queued_at),
676 other => other,
677 }
678 });
679
680 result.truncate(limit);
681 result
682 }
683
684 pub fn get_recent_alerts(&self, limit: usize) -> Vec<DebtAlert> {
686 let alerts = self.recent_alerts.lock();
687 alerts.iter().rev().take(limit).cloned().collect()
688 }
689
690 pub fn clear_old_alerts(&self, max_age: Duration) {
692 let cutoff = saturating_system_time_sub(super::replayable_system_time(), max_age);
693 let mut alerts = self.recent_alerts.lock();
694 alerts.retain(|alert| alert.generated_at > cutoff);
695 }
696
697 pub fn emergency_cleanup(&self, max_age: Duration) -> usize {
699 let cutoff = saturating_system_time_sub(super::replayable_system_time(), max_age);
700 let mut cleaned_count = 0;
701
702 {
703 let mut pending = self.pending_work.lock();
704 for work_map in pending.values_mut() {
705 let before_count = work_map.len();
706 work_map.retain(|_, work| work.queued_at > cutoff);
707 cleaned_count += before_count - work_map.len();
708 }
709 }
710
711 if cleaned_count > 0 {
712 self.generate_alert(DebtAlert {
713 level: DebtAlertLevel::Emergency,
714 message: format!("Emergency cleanup removed {cleaned_count} stale work items"),
715 work_type: None,
716 entity_id: None,
717 metric_value: cleaned_count as f64,
718 threshold: 0.0,
719 generated_at: super::replayable_system_time(),
720 remediation_suggestions: vec![
721 "Investigate why work items are not being processed".to_string(),
722 "Check for deadlocks or blocked entities".to_string(),
723 "Consider increasing processing capacity".to_string(),
724 ],
725 });
726 }
727
728 cleaned_count
729 }
730
731 fn check_debt_levels(&self) {
733 let snapshot = self.get_debt_snapshot();
734 let new_alert_level = self.calculate_alert_level(&snapshot);
735
736 let new_byte = new_alert_level.as_u8();
740 let prev_byte = self.current_alert_level.swap(new_byte, Ordering::AcqRel);
741 if prev_byte != new_byte {
742 let old_level = DebtAlertLevel::from_u8(prev_byte);
743 self.generate_debt_level_alert(old_level, new_alert_level, &snapshot);
744 }
745
746 self.check_threshold_violations(&snapshot);
748 }
749
750 fn calculate_alert_level(&self, snapshot: &DebtSnapshot) -> DebtAlertLevel {
752 if snapshot.memory_usage_mb > (self.config.max_tracking_memory_mb as f64 * 0.9)
754 || snapshot.debt_percentage > 95.0
755 {
756 return DebtAlertLevel::Emergency;
757 }
758
759 if snapshot.debt_percentage > 90.0
761 || (snapshot.processing_rate < self.config.min_processing_rate * 0.1
762 && snapshot.total_pending > 100)
763 {
764 return DebtAlertLevel::Critical;
765 }
766
767 if snapshot.debt_percentage > self.config.debt_threshold_percentage
769 || snapshot.processing_rate < self.config.min_processing_rate * 0.5
770 {
771 return DebtAlertLevel::Warning;
772 }
773
774 if snapshot.debt_percentage > 50.0
776 || snapshot.oldest_work_age > self.config.max_pending_duration * 2
777 {
778 return DebtAlertLevel::Watch;
779 }
780
781 DebtAlertLevel::Normal
782 }
783
784 fn generate_debt_level_alert(
786 &self,
787 _old_level: DebtAlertLevel,
788 new_level: DebtAlertLevel,
789 snapshot: &DebtSnapshot,
790 ) {
791 let message = match new_level {
792 DebtAlertLevel::Emergency => {
793 "EMERGENCY: Cancellation debt overflow detected".to_string()
794 }
795 DebtAlertLevel::Critical => {
796 "CRITICAL: Severe cancellation debt accumulation".to_string()
797 }
798 DebtAlertLevel::Warning => "WARNING: Elevated cancellation debt levels".to_string(),
799 DebtAlertLevel::Watch => "WATCH: Cancellation debt increasing".to_string(),
800 DebtAlertLevel::Normal => "INFO: Cancellation debt levels normal".to_string(),
801 };
802
803 let remediation_suggestions = match new_level {
804 DebtAlertLevel::Emergency => vec![
805 "Execute emergency cleanup immediately".to_string(),
806 "Scale up processing capacity".to_string(),
807 "Investigate system bottlenecks".to_string(),
808 ],
809 DebtAlertLevel::Critical => vec![
810 "Increase cancellation processing rate".to_string(),
811 "Consider work prioritization".to_string(),
812 "Check for deadlocks or stuck entities".to_string(),
813 ],
814 DebtAlertLevel::Warning => vec![
815 "Monitor processing rates closely".to_string(),
816 "Optimize cancellation handlers".to_string(),
817 "Consider load shedding if applicable".to_string(),
818 ],
819 DebtAlertLevel::Watch => vec![
820 "Monitor debt accumulation trends".to_string(),
821 "Verify processing pipeline health".to_string(),
822 ],
823 DebtAlertLevel::Normal => vec!["Continue monitoring".to_string()],
824 };
825
826 self.generate_alert(DebtAlert {
827 level: new_level,
828 message,
829 work_type: None,
830 entity_id: None,
831 metric_value: snapshot.debt_percentage,
832 threshold: match new_level {
833 DebtAlertLevel::Emergency => 95.0,
834 DebtAlertLevel::Critical => 90.0,
835 DebtAlertLevel::Warning => self.config.debt_threshold_percentage,
836 DebtAlertLevel::Watch => 50.0,
837 DebtAlertLevel::Normal => 0.0,
838 },
839 generated_at: snapshot.snapshot_time,
840 remediation_suggestions,
841 });
842 }
843
844 fn check_threshold_violations(&self, snapshot: &DebtSnapshot) {
846 let stats = self.processing_stats.lock();
848 for (work_type, stat) in stats.iter() {
849 if stat.last_rate < self.config.min_processing_rate * 0.1 {
850 self.generate_alert(DebtAlert {
851 level: DebtAlertLevel::Warning,
852 message: format!(
853 "Very slow processing rate for {:?}: {:.1}/sec",
854 work_type, stat.last_rate
855 ),
856 work_type: Some(*work_type),
857 entity_id: None,
858 metric_value: stat.last_rate,
859 threshold: self.config.min_processing_rate * 0.1,
860 generated_at: snapshot.snapshot_time,
861 remediation_suggestions: vec![
862 format!("Optimize {:?} processing handlers", work_type),
863 "Check for blocking operations".to_string(),
864 ],
865 });
866 }
867 }
868
869 for (entity_id, &depth) in &snapshot.entity_queue_depths {
871 if depth > 1000 {
872 self.generate_alert(DebtAlert {
873 level: DebtAlertLevel::Warning,
874 message: format!("Entity {entity_id} has excessive queue depth: {depth}"),
875 work_type: None,
876 entity_id: Some(entity_id.clone()),
877 metric_value: depth as f64,
878 threshold: 1000.0,
879 generated_at: snapshot.snapshot_time,
880 remediation_suggestions: vec![
881 "Investigate entity-specific bottlenecks".to_string(),
882 "Check for resource leaks in entity cleanup".to_string(),
883 ],
884 });
885 }
886 }
887 }
888
889 #[allow(unused_variables)]
891 fn generate_alert(&self, alert: DebtAlert) {
892 {
893 let mut alerts = self.recent_alerts.lock();
894 alerts.push_back(alert.clone());
895
896 while alerts.len() > 1000 {
898 alerts.pop_front();
899 }
900 }
901
902 crate::tracing_compat::warn!(
903 level = ?alert.level,
904 work_type = ?alert.work_type,
905 entity_id = ?alert.entity_id,
906 metric_value = alert.metric_value,
907 threshold = alert.threshold,
908 generated_at = ?alert.generated_at,
909 message = %alert.message,
910 "cancellation debt alert"
911 );
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 #![allow(
918 clippy::pedantic,
919 clippy::nursery,
920 clippy::expect_fun_call,
921 clippy::map_unwrap_or,
922 clippy::cast_possible_wrap,
923 clippy::future_not_send
924 )]
925 use super::*;
926 use crate::types::{CancelKind, CancelReason};
927
928 #[test]
929 fn test_debt_monitor_creation() {
930 let config = CancellationDebtConfig::default();
931 let monitor = CancellationDebtMonitor::new(config);
932
933 let snapshot = monitor.get_debt_snapshot();
934 assert_eq!(snapshot.total_pending, 0);
935 assert_eq!(snapshot.debt_percentage, 0.0);
936 }
937
938 #[test]
939 fn test_work_lifecycle() {
940 let monitor = CancellationDebtMonitor::default();
941
942 let work_id = monitor.queue_work(
943 WorkType::TaskCleanup,
944 "test-task".to_string(),
945 10,
946 100,
947 &CancelReason::user("test"),
948 CancelKind::User,
949 Vec::new(),
950 );
951
952 let snapshot = monitor.get_debt_snapshot();
953 assert_eq!(snapshot.total_pending, 1);
954 assert!(
955 snapshot
956 .pending_by_type
957 .contains_key(&WorkType::TaskCleanup)
958 );
959
960 let completed = monitor.complete_work(work_id);
961 assert!(completed);
962
963 let snapshot = monitor.get_debt_snapshot();
964 assert_eq!(snapshot.total_pending, 0);
965 }
966
967 #[test]
968 fn test_debt_calculation() {
969 let mut config = CancellationDebtConfig::default();
970 config.max_queue_depth = 100;
971 let monitor = CancellationDebtMonitor::new(config);
972
973 for i in 0..75 {
975 monitor.queue_work(
976 WorkType::TaskCleanup,
977 format!("task-{}", i),
978 1,
979 10,
980 &CancelReason::user("test"),
981 CancelKind::User,
982 Vec::new(),
983 );
984 }
985
986 let snapshot = monitor.get_debt_snapshot();
987 assert_eq!(snapshot.total_pending, 75);
988 assert_eq!(snapshot.debt_percentage, 75.0);
989 }
990
991 #[test]
992 fn test_batch_completion() {
993 let monitor = CancellationDebtMonitor::default();
994
995 let work_ids: Vec<u64> = (0..5)
996 .map(|i| {
997 monitor.queue_work(
998 WorkType::ResourceFinalization,
999 format!("resource-{}", i),
1000 1,
1001 50,
1002 &CancelReason::user("batch_test"),
1003 CancelKind::User,
1004 Vec::new(),
1005 )
1006 })
1007 .collect();
1008
1009 let completed = monitor.complete_work_batch(&work_ids);
1010 assert_eq!(completed, 5);
1011
1012 let snapshot = monitor.get_debt_snapshot();
1013 assert_eq!(snapshot.total_pending, 0);
1014 }
1015
1016 #[test]
1017 fn test_priority_work_retrieval() {
1018 let monitor = CancellationDebtMonitor::default();
1019
1020 monitor.queue_work(
1022 WorkType::TaskCleanup,
1023 "low-priority".to_string(),
1024 1,
1025 10,
1026 &CancelReason::user("test"),
1027 CancelKind::User,
1028 Vec::new(),
1029 );
1030
1031 monitor.queue_work(
1032 WorkType::TaskCleanup,
1033 "high-priority".to_string(),
1034 100,
1035 10,
1036 &CancelReason::user("test"),
1037 CancelKind::User,
1038 Vec::new(),
1039 );
1040
1041 let priority_work = monitor.get_priority_work(5);
1042 assert_eq!(priority_work.len(), 2);
1043 assert_eq!(priority_work[0].priority, 100); assert_eq!(priority_work[1].priority, 1);
1045 }
1046
1047 #[test]
1048 fn test_emergency_cleanup() {
1049 let monitor = CancellationDebtMonitor::default();
1050
1051 let work_id = monitor.queue_work(
1054 WorkType::ChannelCleanup,
1055 "old-work".to_string(),
1056 1,
1057 10,
1058 &CancelReason::user("test"),
1059 CancelKind::User,
1060 Vec::new(),
1061 );
1062 {
1063 let mut pending = monitor.pending_work.lock();
1064 let work = pending
1065 .get_mut(&WorkType::ChannelCleanup)
1066 .and_then(|work_map| work_map.get_mut(&work_id))
1067 .expect("queued work must be present");
1068 work.queued_at = SystemTime::UNIX_EPOCH;
1069 }
1070
1071 let cleaned = monitor.emergency_cleanup(Duration::from_millis(1));
1073 assert!(cleaned > 0);
1074
1075 let snapshot = monitor.get_debt_snapshot();
1076 assert_eq!(snapshot.total_pending, 0);
1077 }
1078
1079 #[test]
1083 fn cancel_reason_truncated_at_byte_cap() {
1084 let mut config = CancellationDebtConfig::default();
1085 config.max_cancel_reason_bytes = 16;
1086 let monitor = CancellationDebtMonitor::new(config);
1087
1088 let long = "A".repeat(10_000);
1089 let mut reason = CancelReason::new(CancelKind::User);
1090 reason.message = Some(long);
1091 let id = monitor.queue_work(
1092 WorkType::TaskCleanup,
1093 "entity".into(),
1094 1,
1095 1,
1096 &reason,
1097 CancelKind::User,
1098 Vec::new(),
1099 );
1100
1101 let work = monitor
1102 .get_priority_work(10)
1103 .into_iter()
1104 .find(|w| w.work_id == id)
1105 .expect("queued work present");
1106 assert!(
1109 work.cancel_reason.len() <= 16 + 3,
1110 "cancel_reason exceeded cap: {} bytes",
1111 work.cancel_reason.len()
1112 );
1113 assert!(
1114 work.cancel_reason.ends_with('…'),
1115 "expected ellipsis suffix on truncated reason: {:?}",
1116 work.cancel_reason
1117 );
1118 }
1119
1120 #[test]
1123 fn cancel_reason_short_passes_through() {
1124 let monitor = CancellationDebtMonitor::default();
1125 let reason = CancelReason::user("short");
1126 let id = monitor.queue_work(
1127 WorkType::TaskCleanup,
1128 "entity".into(),
1129 1,
1130 1,
1131 &reason,
1132 CancelKind::User,
1133 Vec::new(),
1134 );
1135 let work = monitor
1136 .get_priority_work(10)
1137 .into_iter()
1138 .find(|w| w.work_id == id)
1139 .unwrap();
1140 assert!(!work.cancel_reason.ends_with('…'));
1141 assert!(!work.cancel_reason.is_empty());
1142 }
1143
1144 #[test]
1147 fn per_work_type_cap_evicts_oldest() {
1148 let mut config = CancellationDebtConfig::default();
1149 config.max_pending_per_work_type = 4;
1150 let monitor = CancellationDebtMonitor::new(config);
1151
1152 let mut ids = Vec::new();
1153 for i in 0..4 {
1154 ids.push(monitor.queue_work(
1155 WorkType::TaskCleanup,
1156 format!("task-{i}"),
1157 1,
1158 1,
1159 &CancelReason::user("x"),
1160 CancelKind::User,
1161 Vec::new(),
1162 ));
1163 }
1164 assert_eq!(monitor.get_debt_snapshot().total_pending, 4);
1165 assert_eq!(monitor.eviction_count(), 0);
1166
1167 let _new_id = monitor.queue_work(
1169 WorkType::TaskCleanup,
1170 "task-5".into(),
1171 1,
1172 1,
1173 &CancelReason::user("x"),
1174 CancelKind::User,
1175 Vec::new(),
1176 );
1177 assert_eq!(monitor.get_debt_snapshot().total_pending, 4);
1178 assert_eq!(monitor.eviction_count(), 1);
1179
1180 for i in 0..4 {
1182 monitor.queue_work(
1183 WorkType::ChannelCleanup,
1184 format!("chan-{i}"),
1185 1,
1186 1,
1187 &CancelReason::user("x"),
1188 CancelKind::User,
1189 Vec::new(),
1190 );
1191 }
1192 assert_eq!(monitor.get_debt_snapshot().total_pending, 8);
1193 assert_eq!(monitor.eviction_count(), 1);
1194 }
1195
1196 #[test]
1200 fn alert_level_atomic_roundtrip() {
1201 for level in [
1202 DebtAlertLevel::Normal,
1203 DebtAlertLevel::Watch,
1204 DebtAlertLevel::Warning,
1205 DebtAlertLevel::Critical,
1206 DebtAlertLevel::Emergency,
1207 ] {
1208 assert_eq!(DebtAlertLevel::from_u8(level.as_u8()), level);
1209 }
1210 assert_eq!(DebtAlertLevel::from_u8(255), DebtAlertLevel::Normal);
1212 assert_eq!(DebtAlertLevel::from_u8(99), DebtAlertLevel::Normal);
1213 }
1214
1215 #[test]
1218 fn truncate_to_bytes_respects_utf8_boundaries() {
1219 let s = "ABC😀DEF";
1221 let out = truncate_to_bytes(s, 5);
1223 assert!(out.is_char_boundary(out.len()));
1224 assert!(out.starts_with("ABC"));
1228 assert!(out.ends_with('…'));
1229 assert_eq!(truncate_to_bytes("hi", 100), "hi");
1231 }
1232
1233 #[test]
1234 fn system_time_windows_do_not_underflow_near_epoch() {
1235 let mut stats = ProcessingStats {
1236 items_processed: VecDeque::new(),
1237 total_processed: AtomicU64::new(0),
1238 last_rate: 0.0,
1239 last_rate_time: SystemTime::UNIX_EPOCH,
1240 };
1241
1242 stats.record_processing(1, SystemTime::UNIX_EPOCH, Duration::MAX);
1243 let rate = stats.calculate_rate(
1244 Duration::MAX,
1245 SystemTime::UNIX_EPOCH + Duration::from_secs(6),
1246 );
1247
1248 assert_eq!(
1249 saturating_system_time_sub(SystemTime::UNIX_EPOCH, Duration::MAX),
1250 SystemTime::UNIX_EPOCH
1251 );
1252 assert!(rate.is_finite());
1253 }
1254
1255 #[test]
1256 fn age_based_cleanup_tolerates_oversized_windows() {
1257 let monitor = CancellationDebtMonitor::default();
1258 let id = monitor.queue_work(
1259 WorkType::ChannelCleanup,
1260 "epoch-safe-work".to_string(),
1261 1,
1262 10,
1263 &CancelReason::user("epoch-safe"),
1264 CancelKind::User,
1265 Vec::new(),
1266 );
1267
1268 monitor.clear_old_alerts(Duration::MAX);
1269 let cleaned = monitor.emergency_cleanup(Duration::MAX);
1270
1271 assert_eq!(cleaned, 0);
1272 assert!(monitor.complete_work(id));
1273 }
1274
1275 #[test]
1280 fn af24n5_entity_queue_depths_cap_with_overflow_bucket() {
1281 let monitor = CancellationDebtMonitor::default();
1282 let cap = super::MAX_QUEUE_DEPTH_ENTITIES;
1283 let total = cap + 100;
1284 let reason = CancelReason::user("af24n5-cap-test");
1285 for i in 0..total {
1286 let _ = monitor.queue_work(
1287 WorkType::TaskCleanup,
1288 format!("entity_{i}"),
1289 10,
1290 1,
1291 &reason,
1292 CancelKind::User,
1293 Vec::new(),
1294 );
1295 }
1296 let snapshot = monitor.get_debt_snapshot();
1297 assert!(
1298 snapshot.entity_queue_depths.len() <= cap + 1,
1299 "entity_queue_depths grew past cap+overflow: {} (cap {cap})",
1300 snapshot.entity_queue_depths.len()
1301 );
1302 assert!(
1303 snapshot
1304 .entity_queue_depths
1305 .contains_key(super::QUEUE_DEPTH_OVERFLOW_BUCKET),
1306 "overflow sentinel must be present once cap is exceeded"
1307 );
1308 }
1309}