1use super::id::Time;
131use crate::tracing_compat::{info, trace};
132use core::fmt;
133use std::time::Duration;
134
135#[derive(Clone, Copy, PartialEq, Eq)]
141pub struct Budget {
142 pub deadline: Option<Time>,
144 pub poll_quota: u32,
146 pub cost_quota: Option<u64>,
148 pub priority: u8,
150}
151
152impl Budget {
153 pub const INFINITE: Self = Self {
155 deadline: None,
156 poll_quota: u32::MAX,
157 cost_quota: None,
158 priority: 0,
159 };
160
161 pub const ZERO: Self = Self {
163 deadline: Some(Time::ZERO),
164 poll_quota: 0,
165 cost_quota: Some(0),
166 priority: 255,
167 };
168
169 pub const MINIMAL: Self = Self {
175 deadline: None,
176 poll_quota: 100,
177 cost_quota: None,
178 priority: 128,
179 };
180
181 #[inline]
183 #[must_use]
184 pub const fn new() -> Self {
185 Self {
186 deadline: None,
187 poll_quota: u32::MAX,
188 cost_quota: None,
189 priority: 128,
190 }
191 }
192
193 #[inline]
205 #[must_use]
206 pub const fn unlimited() -> Self {
207 Self::INFINITE
208 }
209
210 #[inline]
223 #[must_use]
224 pub const fn with_deadline_secs(secs: u64) -> Self {
225 Self {
226 deadline: Some(Time::from_secs(secs)),
227 poll_quota: u32::MAX,
228 cost_quota: None,
229 priority: 128,
230 }
231 }
232
233 #[inline]
244 #[must_use]
245 pub const fn with_deadline_ns(nanos: u64) -> Self {
246 Self {
247 deadline: Some(Time::from_nanos(nanos)),
248 poll_quota: u32::MAX,
249 cost_quota: None,
250 priority: 128,
251 }
252 }
253
254 #[inline]
256 #[must_use]
257 pub const fn with_deadline(mut self, deadline: Time) -> Self {
258 self.deadline = Some(deadline);
259 self
260 }
261
262 #[inline]
264 #[must_use]
265 pub const fn with_poll_quota(mut self, quota: u32) -> Self {
266 self.poll_quota = quota;
267 self
268 }
269
270 #[inline]
272 #[must_use]
273 pub const fn with_cost_quota(mut self, quota: u64) -> Self {
274 self.cost_quota = Some(quota);
275 self
276 }
277
278 #[inline]
280 #[must_use]
281 pub const fn with_priority(mut self, priority: u8) -> Self {
282 self.priority = priority;
283 self
284 }
285
286 #[inline]
290 #[must_use]
291 pub const fn is_exhausted(&self) -> bool {
292 self.poll_quota == 0 || matches!(self.cost_quota, Some(0))
293 }
294
295 #[inline]
297 #[must_use]
298 pub fn is_past_deadline(&self, now: Time) -> bool {
299 self.deadline.is_some_and(|d| now >= d)
300 }
301
302 #[inline]
306 pub fn consume_poll(&mut self) -> Option<u32> {
307 if self.poll_quota > 0 {
308 let old = self.poll_quota;
309 self.poll_quota -= 1;
310 trace!(
311 polls_remaining = self.poll_quota,
312 polls_consumed = 1,
313 "budget poll consumed"
314 );
315 if self.poll_quota == 0 {
316 info!(
317 exhausted_resource = "polls",
318 final_quota = 0,
319 overage_amount = 0,
320 "budget poll quota exhausted"
321 );
322 }
323 Some(old)
324 } else {
325 trace!(
326 polls_remaining = 0,
327 "budget poll consume failed: already exhausted"
328 );
329 None
330 }
331 }
332
333 #[inline]
360 #[must_use]
361 pub fn combine(self, other: Self) -> Self {
362 let combined = Self {
363 deadline: match (self.deadline, other.deadline) {
364 (Some(a), Some(b)) => Some(if a < b { a } else { b }),
365 (Some(a), None) => Some(a),
366 (None, Some(b)) => Some(b),
367 (None, None) => None,
368 },
369 poll_quota: self.poll_quota.min(other.poll_quota),
370 cost_quota: match (self.cost_quota, other.cost_quota) {
371 (Some(a), Some(b)) => Some(a.min(b)),
372 (Some(a), None) => Some(a),
373 (None, Some(b)) => Some(b),
374 (None, None) => None,
375 },
376 priority: self.priority.max(other.priority),
377 };
378
379 let deadline_tightened = match (combined.deadline, self.deadline, other.deadline) {
384 (Some(c), Some(s), _) if c < s => true,
385 (Some(c), _, Some(o)) if c < o => true,
386 (Some(_), None, _) | (Some(_), _, None) => true,
387 _ => false,
388 };
389 let poll_tightened =
390 combined.poll_quota < self.poll_quota || combined.poll_quota < other.poll_quota;
391 let cost_tightened = match (combined.cost_quota, self.cost_quota, other.cost_quota) {
392 (Some(c), Some(s), _) if c < s => true,
393 (Some(c), _, Some(o)) if c < o => true,
394 (Some(_), None, _) | (Some(_), _, None) => true,
395 _ => false,
396 };
397 let priority_tightened =
398 combined.priority > self.priority || combined.priority > other.priority;
399
400 if deadline_tightened || poll_tightened || cost_tightened || priority_tightened {
401 trace!(
402 deadline_tightened,
403 poll_tightened,
404 cost_tightened,
405 self_deadline = ?self.deadline,
406 other_deadline = ?other.deadline,
407 combined_deadline = ?combined.deadline,
408 self_poll_quota = self.poll_quota,
409 other_poll_quota = other.poll_quota,
410 combined_poll_quota = combined.poll_quota,
411 self_cost_quota = ?self.cost_quota,
412 other_cost_quota = ?other.cost_quota,
413 combined_cost_quota = ?combined.cost_quota,
414 self_priority = self.priority,
415 other_priority = other.priority,
416 combined_priority = combined.priority,
417 "budget combined (tightened)"
418 );
419 }
420
421 combined
422 }
423
424 #[inline]
442 #[must_use]
443 pub fn meet(self, other: Self) -> Self {
444 self.combine(other)
445 }
446
447 #[allow(clippy::used_underscore_binding)]
463 #[inline]
464 pub fn consume_cost(&mut self, cost: u64) -> bool {
465 match self.cost_quota {
466 None => {
467 trace!(
468 cost_consumed = cost,
469 cost_remaining = "unlimited",
470 "budget cost consumed (unlimited)"
471 );
472 true }
474 Some(remaining) if remaining >= cost => {
475 let new_remaining = remaining - cost;
476 self.cost_quota = Some(new_remaining);
477 trace!(
478 cost_consumed = cost,
479 cost_remaining = new_remaining,
480 "budget cost consumed"
481 );
482 if new_remaining == 0 {
483 info!(
484 exhausted_resource = "cost",
485 final_quota = 0,
486 overage_amount = 0,
487 "budget cost quota exhausted"
488 );
489 }
490 true
491 }
492 Some(remaining) => {
493 #[cfg(not(feature = "tracing-integration"))]
494 let _ = remaining;
495 trace!(
496 cost_requested = cost,
497 cost_remaining = remaining,
498 "budget cost consume failed: insufficient quota"
499 );
500 false
501 }
502 }
503 }
504
505 #[inline]
522 #[must_use]
523 pub fn remaining_time(&self, now: Time) -> Option<Duration> {
524 self.deadline.and_then(|d| {
525 if now < d {
526 Some(Duration::from_nanos(
527 d.as_nanos().saturating_sub(now.as_nanos()),
528 ))
529 } else {
530 None
531 }
532 })
533 }
534
535 #[inline]
548 #[must_use]
549 pub const fn remaining_polls(&self) -> u32 {
550 self.poll_quota
551 }
552
553 #[inline]
568 #[must_use]
569 pub const fn remaining_cost(&self) -> Option<u64> {
570 self.cost_quota
571 }
572
573 #[inline]
592 #[must_use]
593 pub fn to_timeout(&self, now: Time) -> Option<Duration> {
594 self.remaining_time(now)
595 }
596}
597
598impl Default for Budget {
599 fn default() -> Self {
600 Self::new()
601 }
602}
603
604impl fmt::Debug for Budget {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 let mut d = f.debug_struct("Budget");
607 if let Some(deadline) = self.deadline {
608 d.field("deadline", &deadline);
609 }
610 if self.poll_quota < u32::MAX {
611 d.field("poll_quota", &self.poll_quota);
612 }
613 if let Some(cost) = self.cost_quota {
614 d.field("cost_quota", &cost);
615 }
616 d.field("priority", &self.priority);
617 d.finish()
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq)]
627pub enum CurveError {
628 EmptySamples,
630 NonMonotone {
632 index: usize,
634 prev: u64,
636 next: u64,
638 },
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
649pub struct MinPlusCurve {
650 samples: Vec<u64>,
651 tail_rate: u64,
652}
653
654impl MinPlusCurve {
655 #[inline]
661 pub fn new(samples: Vec<u64>, tail_rate: u64) -> Result<Self, CurveError> {
662 if samples.is_empty() {
663 return Err(CurveError::EmptySamples);
664 }
665
666 for idx in 1..samples.len() {
667 if samples[idx] < samples[idx - 1] {
668 return Err(CurveError::NonMonotone {
669 index: idx,
670 prev: samples[idx - 1],
671 next: samples[idx],
672 });
673 }
674 }
675
676 Ok(Self { samples, tail_rate })
677 }
678
679 #[inline]
684 pub fn from_samples(samples: Vec<u64>) -> Result<Self, CurveError> {
685 Self::new(samples, 0)
686 }
687
688 #[inline]
690 #[must_use]
691 pub fn from_token_bucket(burst: u64, rate: u64, horizon: usize) -> Self {
692 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
693 for t in 0..=horizon {
694 let inc = rate.saturating_mul(t as u64);
695 samples.push(burst.saturating_add(inc));
696 }
697 Self {
698 samples,
699 tail_rate: rate,
700 }
701 }
702
703 #[inline]
705 #[must_use]
706 pub fn from_rate_latency(rate: u64, latency: usize, horizon: usize) -> Self {
707 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
708 for t in 0..=horizon {
709 if t <= latency {
710 samples.push(0);
711 } else {
712 let dt = (t - latency) as u64;
713 samples.push(rate.saturating_mul(dt));
714 }
715 }
716 Self {
717 samples,
718 tail_rate: rate,
719 }
720 }
721
722 #[must_use]
724 #[inline]
725 pub fn horizon(&self) -> usize {
726 self.samples.len().saturating_sub(1)
727 }
728
729 #[must_use]
731 #[inline]
732 pub fn tail_rate(&self) -> u64 {
733 self.tail_rate
734 }
735
736 #[must_use]
738 #[inline]
739 pub fn value_at(&self, t: usize) -> u64 {
740 let horizon = self.horizon();
741 if t <= horizon {
742 self.samples[t]
743 } else {
744 let extra = (t - horizon) as u64;
745 self.samples[horizon].saturating_add(self.tail_rate.saturating_mul(extra))
746 }
747 }
748
749 #[must_use]
751 #[inline]
752 pub fn samples(&self) -> &[u64] {
753 &self.samples
754 }
755
756 #[inline]
760 #[must_use]
761 pub fn min_plus_convolution(&self, other: &Self, horizon: usize) -> Self {
762 let mut samples = Vec::with_capacity(horizon.saturating_add(1));
763 for t in 0..=horizon {
764 let mut best = u64::MAX;
765 for s in 0..=t {
766 let a = self.value_at(s);
767 let b = other.value_at(t - s);
768 let sum = a.saturating_add(b);
769 if sum < best {
770 best = sum;
771 }
772 }
773 samples.push(best);
774 }
775
776 Self {
777 samples,
778 tail_rate: self.tail_rate.min(other.tail_rate),
779 }
780 }
781}
782
783#[derive(Debug, Clone, PartialEq, Eq)]
785pub struct CurveBudget {
786 pub arrival: MinPlusCurve,
788 pub service: MinPlusCurve,
790}
791
792impl CurveBudget {
793 #[must_use]
795 #[inline]
796 pub fn backlog_bound(&self, horizon: usize) -> u64 {
797 backlog_bound(&self.arrival, &self.service, horizon)
798 }
799
800 #[must_use]
804 #[inline]
805 pub fn delay_bound(&self, horizon: usize, max_delay: usize) -> Option<usize> {
806 delay_bound(&self.arrival, &self.service, horizon, max_delay)
807 }
808}
809
810#[inline]
812#[must_use]
813pub fn backlog_bound(arrival: &MinPlusCurve, service: &MinPlusCurve, horizon: usize) -> u64 {
814 let mut worst = 0;
815 for t in 0..=horizon {
816 let demand = arrival.value_at(t);
817 let supply = service.value_at(t);
818 let backlog = demand.saturating_sub(supply);
819 if backlog > worst {
820 worst = backlog;
821 }
822 }
823 worst
824}
825
826#[inline]
830#[must_use]
831pub fn delay_bound(
832 arrival: &MinPlusCurve,
833 service: &MinPlusCurve,
834 horizon: usize,
835 max_delay: usize,
836) -> Option<usize> {
837 let mut worst_delay = 0;
838 for t in 0..=horizon {
839 let demand = arrival.value_at(t);
840 let mut found = None;
841 for d in 0..=max_delay {
842 if service.value_at(t + d) >= demand {
843 found = Some(d);
844 break;
845 }
846 }
847 let delay = found?;
848 if delay > worst_delay {
849 worst_delay = delay;
850 }
851 }
852 Some(worst_delay)
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use serde_json::json;
859
860 fn scrub_budget_event(deadline: Option<Time>) -> &'static str {
861 if deadline.is_some() {
862 "[DEADLINE]"
863 } else {
864 "[NONE]"
865 }
866 }
867
868 #[test]
873 fn infinite_budget_values() {
874 let b = Budget::INFINITE;
875 assert_eq!(b.deadline, None);
876 assert_eq!(b.poll_quota, u32::MAX);
877 assert_eq!(b.cost_quota, None);
878 assert_eq!(b.priority, 0);
879 }
880
881 #[test]
882 fn zero_budget_values() {
883 let b = Budget::ZERO;
884 assert_eq!(b.deadline, Some(Time::ZERO));
885 assert_eq!(b.poll_quota, 0);
886 assert_eq!(b.cost_quota, Some(0));
887 assert_eq!(b.priority, 255);
888 }
889
890 #[test]
891 fn new_returns_default_priority() {
892 let b = Budget::new();
893 assert_eq!(b.priority, 128);
894 assert_ne!(b, Budget::INFINITE);
895 }
896
897 #[test]
898 fn default_returns_new() {
899 assert_eq!(Budget::default(), Budget::new());
900 assert_ne!(Budget::default(), Budget::INFINITE);
901 }
902
903 #[test]
908 fn with_deadline_sets_deadline() {
909 let deadline = Time::from_secs(30);
910 let budget = Budget::new().with_deadline(deadline);
911 assert_eq!(budget.deadline, Some(deadline));
912 }
913
914 #[test]
915 fn with_poll_quota_sets_quota() {
916 let budget = Budget::new().with_poll_quota(42);
917 assert_eq!(budget.poll_quota, 42);
918 }
919
920 #[test]
921 fn with_cost_quota_sets_quota() {
922 let budget = Budget::new().with_cost_quota(1000);
923 assert_eq!(budget.cost_quota, Some(1000));
924 }
925
926 #[test]
927 fn with_priority_sets_priority() {
928 let budget = Budget::new().with_priority(255);
929 assert_eq!(budget.priority, 255);
930 }
931
932 #[test]
933 fn builder_chaining() {
934 let budget = Budget::new()
935 .with_deadline(Time::from_secs(10))
936 .with_poll_quota(100)
937 .with_cost_quota(5000)
938 .with_priority(200);
939
940 assert_eq!(budget.deadline, Some(Time::from_secs(10)));
941 assert_eq!(budget.poll_quota, 100);
942 assert_eq!(budget.cost_quota, Some(5000));
943 assert_eq!(budget.priority, 200);
944 }
945
946 #[test]
951 fn is_exhausted_false_for_infinite() {
952 assert!(!Budget::INFINITE.is_exhausted());
953 }
954
955 #[test]
956 fn is_exhausted_true_for_zero() {
957 assert!(Budget::ZERO.is_exhausted());
958 }
959
960 #[test]
961 fn is_exhausted_when_poll_quota_zero() {
962 let budget = Budget::new().with_poll_quota(0);
963 assert!(budget.is_exhausted());
964 }
965
966 #[test]
967 fn is_exhausted_when_cost_quota_zero() {
968 let budget = Budget::new().with_cost_quota(0);
969 assert!(budget.is_exhausted());
970 }
971
972 #[test]
973 fn is_exhausted_false_with_resources() {
974 let budget = Budget::new().with_poll_quota(10).with_cost_quota(100);
975 assert!(!budget.is_exhausted());
976 }
977
978 #[test]
983 fn is_past_deadline_true_when_past() {
984 let budget = Budget::new().with_deadline(Time::from_secs(5));
985 let now = Time::from_secs(10);
986 assert!(budget.is_past_deadline(now));
987 }
988
989 #[test]
990 fn is_past_deadline_false_when_not_past() {
991 let budget = Budget::new().with_deadline(Time::from_secs(10));
992 let now = Time::from_secs(5);
993 assert!(!budget.is_past_deadline(now));
994 }
995
996 #[test]
997 fn is_past_deadline_true_at_exact_time() {
998 let deadline = Time::from_secs(5);
999 let budget = Budget::new().with_deadline(deadline);
1000 assert!(budget.is_past_deadline(deadline));
1001 }
1002
1003 #[test]
1004 fn is_past_deadline_false_when_no_deadline() {
1005 let budget = Budget::new();
1006 assert!(!budget.is_past_deadline(Time::from_secs(1_000_000)));
1007 }
1008
1009 #[test]
1014 fn consume_poll_decrements() {
1015 let mut budget = Budget::new().with_poll_quota(2);
1016
1017 assert_eq!(budget.consume_poll(), Some(2));
1018 assert_eq!(budget.poll_quota, 1);
1019
1020 assert_eq!(budget.consume_poll(), Some(1));
1021 assert_eq!(budget.poll_quota, 0);
1022
1023 assert_eq!(budget.consume_poll(), None);
1024 assert_eq!(budget.poll_quota, 0);
1025 }
1026
1027 #[test]
1028 fn consume_poll_returns_none_at_zero() {
1029 let mut budget = Budget::new().with_poll_quota(0);
1030 assert_eq!(budget.consume_poll(), None);
1031 assert_eq!(budget.poll_quota, 0);
1032 }
1033
1034 #[test]
1035 fn consume_poll_transitions_to_exhausted() {
1036 let mut budget = Budget::new().with_poll_quota(1);
1037 assert!(!budget.is_exhausted());
1038
1039 budget.consume_poll();
1040 assert!(budget.is_exhausted());
1041 }
1042
1043 #[test]
1048 fn combine_takes_tighter() {
1049 let a = Budget::new()
1050 .with_deadline(Time::from_secs(10))
1051 .with_poll_quota(100)
1052 .with_priority(50);
1053
1054 let b = Budget::new()
1055 .with_deadline(Time::from_secs(5))
1056 .with_poll_quota(200)
1057 .with_priority(100);
1058
1059 let combined = a.combine(b);
1060
1061 assert_eq!(combined.deadline, Some(Time::from_secs(5)));
1063 assert_eq!(combined.poll_quota, 100);
1065 assert_eq!(combined.priority, 100);
1067 }
1068
1069 #[test]
1070 fn combine_deadline_none_with_some() {
1071 let a = Budget::new(); let b = Budget::new().with_deadline(Time::from_secs(5));
1073
1074 assert_eq!(a.combine(b).deadline, Some(Time::from_secs(5)));
1076 assert_eq!(b.combine(a).deadline, Some(Time::from_secs(5)));
1078 }
1079
1080 #[test]
1081 fn combine_deadline_none_with_none() {
1082 let a = Budget::new();
1083 let b = Budget::new();
1084 assert_eq!(a.combine(b).deadline, None);
1085 }
1086
1087 #[test]
1088 fn combine_cost_quota_none_with_some() {
1089 let a = Budget::new(); let b = Budget::new().with_cost_quota(100);
1091
1092 assert_eq!(a.combine(b).cost_quota, Some(100));
1094 assert_eq!(b.combine(a).cost_quota, Some(100));
1095 }
1096
1097 #[test]
1098 fn combine_cost_quota_takes_min() {
1099 let a = Budget::new().with_cost_quota(50);
1100 let b = Budget::new().with_cost_quota(100);
1101
1102 assert_eq!(a.combine(b).cost_quota, Some(50));
1103 assert_eq!(b.combine(a).cost_quota, Some(50));
1104 }
1105
1106 #[test]
1107 fn combine_with_zero_absorbs() {
1108 let any_budget = Budget::new()
1109 .with_deadline(Time::from_secs(100))
1110 .with_poll_quota(1000)
1111 .with_cost_quota(10000)
1112 .with_priority(200);
1113
1114 let combined = any_budget.combine(Budget::ZERO);
1115
1116 assert_eq!(combined.deadline, Some(Time::ZERO));
1118 assert_eq!(combined.poll_quota, 0);
1120 assert_eq!(combined.cost_quota, Some(0));
1122 assert_eq!(combined.priority, 255);
1124 }
1125
1126 #[test]
1127 fn combine_with_infinite_preserves() {
1128 let budget = Budget::new()
1129 .with_deadline(Time::from_secs(10))
1130 .with_poll_quota(100)
1131 .with_cost_quota(1000)
1132 .with_priority(50);
1133
1134 let combined = budget.combine(Budget::INFINITE);
1135
1136 assert_eq!(combined.deadline, Some(Time::from_secs(10)));
1138 assert_eq!(combined.poll_quota, 100);
1139 assert_eq!(combined.cost_quota, Some(1000));
1140 assert_eq!(combined.priority, 50);
1142 }
1143
1144 #[test]
1149 fn debug_shows_constrained_fields() {
1150 let budget = Budget::new()
1151 .with_deadline(Time::from_secs(10))
1152 .with_poll_quota(100)
1153 .with_cost_quota(500);
1154
1155 let debug = format!("{budget:?}");
1156
1157 assert!(debug.contains("deadline"));
1159 assert!(debug.contains("poll_quota"));
1160 assert!(debug.contains("cost_quota"));
1161 assert!(debug.contains("priority"));
1162 }
1163
1164 #[test]
1165 fn debug_omits_unconstrained_fields() {
1166 let budget = Budget::INFINITE;
1167 let debug = format!("{budget:?}");
1168
1169 assert!(!debug.contains("deadline"));
1172 assert!(!debug.contains("poll_quota")); assert!(debug.contains("priority"));
1174 }
1175
1176 #[test]
1181 fn equality_works() {
1182 let a = Budget::new()
1183 .with_deadline(Time::from_secs(10))
1184 .with_poll_quota(100);
1185
1186 let b = Budget::new()
1187 .with_deadline(Time::from_secs(10))
1188 .with_poll_quota(100);
1189
1190 assert_eq!(a, b);
1191 }
1192
1193 #[test]
1194 fn inequality_on_deadline() {
1195 let a = Budget::new().with_deadline(Time::from_secs(10));
1196 let b = Budget::new().with_deadline(Time::from_secs(20));
1197 assert_ne!(a, b);
1198 }
1199
1200 #[test]
1201 fn copy_semantics() {
1202 let a = Budget::new().with_poll_quota(100);
1203 let b = a; assert_eq!(a.poll_quota, b.poll_quota);
1205 let mut c = a;
1207 c.poll_quota = 50;
1208 assert_eq!(a.poll_quota, 100);
1209 assert_eq!(c.poll_quota, 50);
1210 }
1211
1212 #[test]
1217 fn unlimited_returns_infinite() {
1218 assert_eq!(Budget::unlimited(), Budget::INFINITE);
1219 }
1220
1221 #[test]
1222 fn with_deadline_secs_constructor() {
1223 let budget = Budget::with_deadline_secs(30);
1224 assert_eq!(budget.deadline, Some(Time::from_secs(30)));
1225 assert_eq!(budget.poll_quota, u32::MAX);
1226 assert_eq!(budget.cost_quota, None);
1227 assert_eq!(budget.priority, 128);
1228 }
1229
1230 #[test]
1231 fn with_deadline_ns_constructor() {
1232 let budget = Budget::with_deadline_ns(30_000_000_000);
1233 assert_eq!(budget.deadline, Some(Time::from_nanos(30_000_000_000)));
1234 }
1235
1236 #[test]
1237 fn meet_is_alias_for_combine() {
1238 let a = Budget::new()
1239 .with_deadline(Time::from_secs(10))
1240 .with_poll_quota(100);
1241
1242 let b = Budget::new()
1243 .with_deadline(Time::from_secs(5))
1244 .with_poll_quota(200);
1245
1246 assert_eq!(a.meet(b), a.combine(b));
1247 }
1248
1249 #[test]
1254 fn consume_cost_basic() {
1255 let mut budget = Budget::new().with_cost_quota(100);
1256
1257 assert!(budget.consume_cost(30));
1258 assert_eq!(budget.cost_quota, Some(70));
1259
1260 assert!(budget.consume_cost(70));
1261 assert_eq!(budget.cost_quota, Some(0));
1262
1263 assert!(!budget.consume_cost(1));
1264 assert_eq!(budget.cost_quota, Some(0));
1265 }
1266
1267 #[test]
1268 fn consume_cost_unlimited() {
1269 let mut budget = Budget::new(); assert!(budget.consume_cost(1_000_000));
1271 assert_eq!(budget.cost_quota, None);
1272 }
1273
1274 #[test]
1275 fn consume_cost_exact_amount() {
1276 let mut budget = Budget::new().with_cost_quota(50);
1277 assert!(budget.consume_cost(50));
1278 assert_eq!(budget.cost_quota, Some(0));
1279 }
1280
1281 #[test]
1282 fn consume_cost_transitions_to_exhausted() {
1283 let mut budget = Budget::new().with_cost_quota(10).with_poll_quota(u32::MAX);
1284 assert!(!budget.is_exhausted());
1285
1286 budget.consume_cost(10);
1287 assert!(budget.is_exhausted());
1288 }
1289
1290 #[test]
1295 fn remaining_time_basic() {
1296 let budget = Budget::with_deadline_secs(30);
1297 let now = Time::from_secs(10);
1298
1299 let remaining = budget.remaining_time(now);
1300 assert_eq!(remaining, Some(Duration::from_secs(20)));
1301 }
1302
1303 #[test]
1304 fn remaining_time_no_deadline() {
1305 let budget = Budget::unlimited();
1306 assert_eq!(budget.remaining_time(Time::from_secs(1000)), None);
1307 }
1308
1309 #[test]
1310 fn remaining_time_past_deadline() {
1311 let budget = Budget::with_deadline_secs(10);
1312 assert_eq!(budget.remaining_time(Time::from_secs(15)), None);
1313 }
1314
1315 #[test]
1316 fn remaining_time_at_deadline() {
1317 let budget = Budget::with_deadline_secs(10);
1318 assert_eq!(budget.remaining_time(Time::from_secs(10)), None);
1319 }
1320
1321 #[test]
1322 fn remaining_polls_basic() {
1323 let budget = Budget::new().with_poll_quota(100);
1324 assert_eq!(budget.remaining_polls(), 100);
1325 }
1326
1327 #[test]
1328 fn remaining_polls_unlimited() {
1329 let budget = Budget::unlimited();
1330 assert_eq!(budget.remaining_polls(), u32::MAX);
1331 }
1332
1333 #[test]
1334 fn remaining_cost_basic() {
1335 let budget = Budget::new().with_cost_quota(1000);
1336 assert_eq!(budget.remaining_cost(), Some(1000));
1337 }
1338
1339 #[test]
1340 fn remaining_cost_unlimited() {
1341 let budget = Budget::unlimited();
1342 assert_eq!(budget.remaining_cost(), None);
1343 }
1344
1345 #[test]
1346 fn to_timeout_is_alias_for_remaining_time() {
1347 let budget = Budget::with_deadline_secs(30);
1348 let now = Time::from_secs(10);
1349
1350 assert_eq!(budget.to_timeout(now), budget.remaining_time(now));
1351 }
1352
1353 #[test]
1358 fn min_plus_curve_validation_rejects_non_monotone() {
1359 let err = MinPlusCurve::new(vec![0, 2, 1], 0).expect_err("non-monotone samples");
1360 assert!(matches!(
1361 err,
1362 CurveError::NonMonotone {
1363 index: 2,
1364 prev: 2,
1365 next: 1
1366 }
1367 ));
1368 }
1369
1370 #[test]
1371 fn min_plus_convolution_basic() {
1372 let a = MinPlusCurve::new(vec![0, 1, 2], 1).expect("valid curve");
1373 let b = MinPlusCurve::new(vec![0, 0, 1], 1).expect("valid curve");
1374 let conv = a.min_plus_convolution(&b, 2);
1375 assert_eq!(conv.samples(), &[0, 0, 1]);
1376 }
1377
1378 #[test]
1379 fn min_plus_backlog_delay_demo() {
1380 let arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
1381 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
1382 let budget = CurveBudget { arrival, service };
1383
1384 let backlog = budget.backlog_bound(10);
1385 let delay = budget.delay_bound(10, 10);
1386
1387 assert_eq!(backlog, 9);
1388 assert_eq!(delay, Some(4));
1389 }
1390
1391 #[test]
1424 fn lemma_meet_commutative() {
1425 let a = Budget::new()
1426 .with_deadline(Time::from_secs(10))
1427 .with_poll_quota(100)
1428 .with_cost_quota(500)
1429 .with_priority(50);
1430
1431 let b = Budget::new()
1432 .with_deadline(Time::from_secs(5))
1433 .with_poll_quota(200)
1434 .with_cost_quota(300)
1435 .with_priority(100);
1436
1437 assert_eq!(a.meet(b), b.meet(a));
1438 }
1439
1440 #[test]
1441 fn lemma_meet_commutative_with_none_deadline() {
1442 let a = Budget::new().with_poll_quota(100);
1443 let b = Budget::new()
1444 .with_deadline(Time::from_secs(5))
1445 .with_poll_quota(200);
1446 assert_eq!(a.meet(b), b.meet(a));
1447 }
1448
1449 #[test]
1450 fn lemma_meet_commutative_with_none_cost() {
1451 let a = Budget::new().with_cost_quota(100);
1452 let b = Budget::new(); assert_eq!(a.meet(b), b.meet(a));
1454 }
1455
1456 #[test]
1460 fn lemma_meet_associative() {
1461 let a = Budget::new()
1462 .with_deadline(Time::from_secs(10))
1463 .with_poll_quota(100)
1464 .with_cost_quota(500)
1465 .with_priority(50);
1466
1467 let b = Budget::new()
1468 .with_deadline(Time::from_secs(5))
1469 .with_poll_quota(200)
1470 .with_cost_quota(300)
1471 .with_priority(100);
1472
1473 let c = Budget::new()
1474 .with_deadline(Time::from_secs(8))
1475 .with_poll_quota(150)
1476 .with_cost_quota(400)
1477 .with_priority(75);
1478
1479 assert_eq!(a.meet(b.meet(c)), a.meet(b).meet(c));
1480 }
1481
1482 #[test]
1483 fn lemma_meet_associative_with_none_fields() {
1484 let a = Budget::new().with_deadline(Time::from_secs(10));
1485 let b = Budget::new().with_cost_quota(300);
1486 let c = Budget::new().with_poll_quota(50);
1487
1488 assert_eq!(a.meet(b.meet(c)), a.meet(b).meet(c));
1489 }
1490
1491 #[test]
1495 fn lemma_meet_idempotent() {
1496 let a = Budget::new()
1497 .with_deadline(Time::from_secs(10))
1498 .with_poll_quota(100)
1499 .with_cost_quota(500)
1500 .with_priority(75);
1501 assert_eq!(a.meet(a), a);
1502 }
1503
1504 #[test]
1505 fn lemma_meet_idempotent_infinite() {
1506 assert_eq!(Budget::INFINITE.meet(Budget::INFINITE), Budget::INFINITE);
1507 }
1508
1509 #[test]
1510 fn lemma_meet_idempotent_zero() {
1511 assert_eq!(Budget::ZERO.meet(Budget::ZERO), Budget::ZERO);
1512 }
1513
1514 #[test]
1518 fn lemma_identity_left() {
1519 let a = Budget::new()
1520 .with_deadline(Time::from_secs(10))
1521 .with_poll_quota(100)
1522 .with_cost_quota(500)
1523 .with_priority(50);
1524
1525 let result = Budget::INFINITE.meet(a);
1528 assert_eq!(result.deadline, a.deadline);
1529 assert_eq!(result.poll_quota, a.poll_quota);
1530 assert_eq!(result.cost_quota, a.cost_quota);
1531 assert_eq!(result.priority, a.priority);
1532 }
1533
1534 #[test]
1535 fn lemma_identity_right() {
1536 let a = Budget::new()
1537 .with_deadline(Time::from_secs(10))
1538 .with_poll_quota(100)
1539 .with_cost_quota(500)
1540 .with_priority(200);
1541
1542 let result = a.meet(Budget::INFINITE);
1543 assert_eq!(result.deadline, a.deadline);
1544 assert_eq!(result.poll_quota, a.poll_quota);
1545 assert_eq!(result.cost_quota, a.cost_quota);
1546 }
1547
1548 #[test]
1554 fn lemma_zero_absorbing_quotas() {
1555 let a = Budget::new()
1556 .with_deadline(Time::from_secs(100))
1557 .with_poll_quota(1000)
1558 .with_cost_quota(10000);
1559
1560 let result = a.meet(Budget::ZERO);
1561 assert_eq!(result.deadline, Some(Time::ZERO));
1562 assert_eq!(result.poll_quota, 0);
1563 assert_eq!(result.cost_quota, Some(0));
1564 }
1565
1566 #[test]
1567 fn lemma_zero_absorbing_symmetric() {
1568 let a = Budget::new()
1569 .with_deadline(Time::from_secs(100))
1570 .with_poll_quota(1000)
1571 .with_cost_quota(10000);
1572
1573 let left = a.meet(Budget::ZERO);
1574 let right = Budget::ZERO.meet(a);
1575 assert_eq!(left.deadline, right.deadline);
1576 assert_eq!(left.poll_quota, right.poll_quota);
1577 assert_eq!(left.cost_quota, right.cost_quota);
1578 }
1579
1580 #[test]
1586 fn lemma_meet_monotone_poll() {
1587 let a = Budget::new().with_poll_quota(100);
1588 let b = Budget::new().with_poll_quota(200);
1589 let result = a.meet(b);
1590 assert!(result.poll_quota <= a.poll_quota);
1591 assert!(result.poll_quota <= b.poll_quota);
1592 }
1593
1594 #[test]
1595 fn lemma_meet_monotone_cost() {
1596 let a = Budget::new().with_cost_quota(100);
1597 let b = Budget::new().with_cost_quota(200);
1598 let result = a.meet(b);
1599 assert!(result.cost_quota.unwrap() <= a.cost_quota.unwrap());
1600 assert!(result.cost_quota.unwrap() <= b.cost_quota.unwrap());
1601 }
1602
1603 #[test]
1604 fn lemma_meet_monotone_deadline() {
1605 let a = Budget::new().with_deadline(Time::from_secs(10));
1606 let b = Budget::new().with_deadline(Time::from_secs(20));
1607 let result = a.meet(b);
1608 assert!(result.deadline.unwrap() <= a.deadline.unwrap());
1609 assert!(result.deadline.unwrap() <= b.deadline.unwrap());
1610 }
1611
1612 #[test]
1616 fn lemma_consume_poll_strictly_decreasing() {
1617 let mut budget = Budget::new().with_poll_quota(5);
1618 let mut prev = budget.poll_quota;
1619 while budget.poll_quota > 0 {
1620 let old = budget.consume_poll().expect("quota > 0");
1621 assert_eq!(old, prev);
1622 assert!(budget.poll_quota < prev);
1623 prev = budget.poll_quota;
1624 }
1625 assert_eq!(budget.consume_poll(), None);
1627 }
1628
1629 #[test]
1633 fn lemma_consume_cost_strictly_decreasing() {
1634 let mut budget = Budget::new().with_cost_quota(100);
1635 let initial = budget.cost_quota.unwrap();
1636 assert!(budget.consume_cost(30));
1637 let after = budget.cost_quota.unwrap();
1638 assert!(after < initial);
1639 assert_eq!(after, 70);
1640 }
1641
1642 #[test]
1646 fn lemma_sufficient_budget_enables_progress() {
1647 let mut budget = Budget::new().with_poll_quota(10).with_cost_quota(100);
1648 assert!(!budget.is_exhausted());
1649
1650 assert!(budget.consume_poll().is_some());
1652 assert!(budget.consume_cost(1));
1654 }
1655
1656 #[test]
1657 fn lemma_exhausted_blocks_progress() {
1658 let mut budget = Budget::new().with_poll_quota(0);
1659 assert!(budget.is_exhausted());
1660 assert!(budget.consume_poll().is_none());
1661 }
1662
1663 #[test]
1668 fn lemma_poll_termination() {
1669 let q = 7u32;
1670 let mut budget = Budget::new().with_poll_quota(q);
1671 for _ in 0..q {
1672 assert!(!budget.is_exhausted());
1673 budget.consume_poll().expect("should succeed");
1674 }
1675 assert!(budget.is_exhausted());
1676 assert_eq!(budget.poll_quota, 0);
1677 }
1678
1679 #[test]
1680 fn lemma_cost_termination() {
1681 let mut budget = Budget::new().with_cost_quota(100);
1682 for _ in 0..4 {
1684 assert!(budget.consume_cost(25));
1685 }
1686 assert_eq!(budget.cost_quota, Some(0));
1687 assert!(!budget.consume_cost(1));
1688 }
1689
1690 #[test]
1694 fn lemma_convolution_associative() {
1695 let a = MinPlusCurve::new(vec![0, 1, 3], 2).expect("valid");
1696 let b = MinPlusCurve::new(vec![0, 2, 4], 2).expect("valid");
1697 let c = MinPlusCurve::new(vec![0, 1, 2], 1).expect("valid");
1698 let h = 4;
1699
1700 let ab_c = a.min_plus_convolution(&b, h).min_plus_convolution(&c, h);
1701 let a_bc = a.min_plus_convolution(&b.min_plus_convolution(&c, h), h);
1702
1703 for t in 0..=h {
1704 assert_eq!(
1705 ab_c.value_at(t),
1706 a_bc.value_at(t),
1707 "associativity failed at t={t}"
1708 );
1709 }
1710 }
1711
1712 #[test]
1716 fn lemma_convolution_commutative() {
1717 let a = MinPlusCurve::new(vec![0, 1, 3], 2).expect("valid");
1718 let b = MinPlusCurve::new(vec![0, 2, 4], 2).expect("valid");
1719 let h = 4;
1720
1721 let ab = a.min_plus_convolution(&b, h);
1722 let ba = b.min_plus_convolution(&a, h);
1723
1724 for t in 0..=h {
1725 assert_eq!(
1726 ab.value_at(t),
1727 ba.value_at(t),
1728 "commutativity failed at t={t}"
1729 );
1730 }
1731 }
1732
1733 #[test]
1737 fn lemma_backlog_monotone_arrival() {
1738 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
1739 let small_arrival = MinPlusCurve::from_token_bucket(2, 1, 10);
1740 let large_arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
1741
1742 let small_backlog = backlog_bound(&small_arrival, &service, 10);
1743 let large_backlog = backlog_bound(&large_arrival, &service, 10);
1744
1745 assert!(large_backlog >= small_backlog);
1746 }
1747
1748 #[test]
1752 fn lemma_delay_monotone_arrival() {
1753 let service = MinPlusCurve::from_rate_latency(3, 2, 10);
1754 let small_arrival = MinPlusCurve::from_token_bucket(2, 1, 10);
1755 let large_arrival = MinPlusCurve::from_token_bucket(5, 2, 10);
1756
1757 let small_delay = delay_bound(&small_arrival, &service, 10, 20).unwrap_or(0);
1758 let large_delay = delay_bound(&large_arrival, &service, 10, 20).unwrap_or(0);
1759
1760 assert!(large_delay >= small_delay);
1761 }
1762
1763 #[test]
1768 fn lemma_convolution_tail_rate_is_min() {
1769 let slow = MinPlusCurve::new(vec![0, 1], 1).expect("valid");
1770 let fast = MinPlusCurve::new(vec![0, 3], 3).expect("valid");
1771 let conv = slow.min_plus_convolution(&fast, 10);
1772
1773 assert_eq!(conv.tail_rate(), 1);
1775
1776 let t = 20;
1779 let mut brute = u64::MAX;
1780 for s in 0..=t {
1781 brute = brute.min(slow.value_at(s).saturating_add(fast.value_at(t - s)));
1782 }
1783 assert_eq!(conv.value_at(t), brute);
1784 }
1785
1786 #[test]
1789 fn budget_clone_copy() {
1790 let b = Budget::new().with_poll_quota(42);
1791 let b2 = b; let b3 = b;
1793 assert_eq!(b, b2);
1794 assert_eq!(b2, b3);
1795 }
1796
1797 #[test]
1798 fn curve_error_debug_clone_eq() {
1799 let e1 = CurveError::EmptySamples;
1800 let e2 = e1.clone();
1801 assert_eq!(e1, e2);
1802
1803 let e3 = CurveError::NonMonotone {
1804 index: 2,
1805 prev: 10,
1806 next: 5,
1807 };
1808 let e4 = e3.clone();
1809 assert_eq!(e3, e4);
1810 assert_ne!(e1, e3);
1811 let dbg = format!("{e3:?}");
1812 assert!(dbg.contains("NonMonotone"));
1813 }
1814
1815 #[test]
1816 fn min_plus_curve_debug_clone_eq() {
1817 let c1 = MinPlusCurve::new(vec![0, 1, 2], 1).unwrap();
1818 let c2 = c1.clone();
1819 assert_eq!(c1, c2);
1820 let dbg = format!("{c1:?}");
1821 assert!(dbg.contains("MinPlusCurve"));
1822 }
1823
1824 #[test]
1825 fn curve_budget_debug_clone_eq() {
1826 let arrival = MinPlusCurve::new(vec![0, 2, 4], 2).unwrap();
1827 let service = MinPlusCurve::new(vec![0, 3, 6], 3).unwrap();
1828 let cb = CurveBudget { arrival, service };
1829 let cb2 = cb.clone();
1830 assert_eq!(cb, cb2);
1831 let dbg = format!("{cb:?}");
1832 assert!(dbg.contains("CurveBudget"));
1833 }
1834
1835 #[test]
1836 fn budget_event_snapshot_scrubbed() {
1837 let mut budget = Budget::new()
1838 .with_deadline(Time::from_secs(12))
1839 .with_poll_quota(3)
1840 .with_cost_quota(40)
1841 .with_priority(180);
1842
1843 let before = budget;
1844 let poll_before = budget.consume_poll();
1845 let after_poll = budget;
1846 let cost_ok = budget.consume_cost(15);
1847 let after_cost = budget;
1848
1849 insta::assert_json_snapshot!(
1850 "budget_event_scrubbed",
1851 json!({
1852 "events": [
1853 {
1854 "event": "created",
1855 "deadline": scrub_budget_event(before.deadline),
1856 "poll_quota": before.poll_quota,
1857 "cost_quota": before.cost_quota,
1858 "priority": before.priority,
1859 },
1860 {
1861 "event": "consume_poll",
1862 "returned": poll_before,
1863 "poll_quota_after": after_poll.poll_quota,
1864 },
1865 {
1866 "event": "consume_cost",
1867 "accepted": cost_ok,
1868 "cost_quota_after": after_cost.cost_quota,
1869 "exhausted": after_cost.is_exhausted(),
1870 }
1871 ]
1872 })
1873 );
1874 }
1875}