1use std::fmt;
16use std::sync::atomic::AtomicU64;
17use std::sync::atomic::AtomicUsize;
18use std::sync::atomic::Ordering;
19use std::sync::Arc;
20use std::sync::Mutex;
21use std::sync::MutexGuard;
22
23use smallvec::SmallVec;
24use std::time::Duration;
25use tokio::sync::Notify;
26
27use super::clock::MonotonicClock;
28use super::clock::SystemMonotonicClock;
29use super::dynamic::DynamicBudgetKey;
30use super::dynamic::DynamicKeyRegistrationFailure;
31use super::dynamic::DynamicKeyRegistry;
32use super::limit::BudgetClass;
33use super::limit::BudgetDimension;
34use super::limit::BudgetLimit;
35use super::limit::FullPolicy;
36use super::limit::RateLimit;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BudgetSnapshot {
41 pub path: Arc<str>,
43 pub current_count: usize,
45 pub current_bytes: usize,
47 pub admitted_count: u64,
49 pub released_count: u64,
51 pub rejected_count: u64,
53 pub throttled_count: u64,
55 pub dropped_count: u64,
57 pub coalesced_count: u64,
59 pub closed_slow_consumer_count: u64,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct BudgetRejection {
66 path: Arc<str>,
67 exhausted_path: Arc<str>,
68 reason: BudgetRejectionReason,
69 policy: FullPolicy,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum BudgetRejectionReason {
75 Capacity(BudgetDimension),
77 Closed,
79}
80
81impl BudgetRejection {
82 #[must_use]
83 pub fn path(&self) -> &str {
85 &self.path
86 }
87
88 #[must_use]
89 pub fn exhausted_path(&self) -> &str {
91 &self.exhausted_path
92 }
93
94 #[must_use]
95 pub const fn dimension(&self) -> Option<BudgetDimension> {
97 match self.reason {
98 BudgetRejectionReason::Capacity(dimension) => Some(dimension),
99 BudgetRejectionReason::Closed => None,
100 }
101 }
102
103 #[must_use]
105 pub const fn reason(&self) -> BudgetRejectionReason {
106 self.reason
107 }
108
109 #[must_use]
111 pub const fn is_closed(&self) -> bool {
112 matches!(self.reason, BudgetRejectionReason::Closed)
113 }
114
115 #[must_use]
116 pub const fn policy(&self) -> FullPolicy {
118 self.policy
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum PermitRebindOutcome {
125 Rebound,
127 Unchanged,
129 Rejected(BudgetRejection),
131}
132
133pub struct ResourceBudgetTree {
135 root: ResourceBudget,
136}
137
138impl fmt::Debug for ResourceBudgetTree {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter
141 .debug_struct("ResourceBudgetTree")
142 .field("root", &self.root)
143 .finish()
144 }
145}
146
147impl ResourceBudgetTree {
148 pub fn new(name: impl Into<String>, limit: BudgetLimit) -> Result<Self, crate::RuntimeContractViolation> {
155 Self::with_clock(name, limit, Arc::new(SystemMonotonicClock::new()))
156 }
157
158 pub fn with_clock(
165 name: impl Into<String>,
166 limit: BudgetLimit,
167 clock: Arc<dyn MonotonicClock>,
168 ) -> Result<Self, crate::RuntimeContractViolation> {
169 let name = validated_name(name.into())?;
170 limit.validate(&name)?;
171 let admission_gates: Arc<[Arc<AdmissionGate>]> = Arc::from([]);
172 let node = Arc::new(BudgetNode::new(
173 Arc::from(name.as_str()),
174 limit,
175 clock,
176 admission_gates.clone(),
177 ));
178 let capacity_notify = Arc::new(Notify::new());
179 Ok(Self {
180 root: ResourceBudget {
181 node: Arc::clone(&node),
182 chain: Arc::from([node]),
183 capacity_notify,
184 keys: DynamicKeyRegistry::new(),
185 admission_gates,
186 },
187 })
188 }
189
190 pub fn with_clock_and_key_capacity(
198 name: impl Into<String>,
199 limit: BudgetLimit,
200 clock: Arc<dyn MonotonicClock>,
201 max_entries: usize,
202 ) -> Result<Self, crate::RuntimeContractViolation> {
203 let mut tree = Self::with_clock(name, limit, clock)?;
204 tree.root.keys = DynamicKeyRegistry::with_max_entries(max_entries);
205 Ok(tree)
206 }
207
208 #[must_use]
209 pub fn root(&self) -> ResourceBudget {
211 self.root.clone()
212 }
213}
214
215#[derive(Clone)]
217pub struct ResourceBudget {
218 node: Arc<BudgetNode>,
219 chain: Arc<[Arc<BudgetNode>]>,
220 capacity_notify: Arc<Notify>,
221 keys: DynamicKeyRegistry,
222 admission_gates: Arc<[Arc<AdmissionGate>]>,
223}
224
225struct AdmissionGate {
226 path: Arc<str>,
227 closed: Mutex<bool>,
228}
229
230pub(super) struct BudgetAdmission<'a> {
234 budget: &'a ResourceBudget,
235 _guards: SmallVec<[MutexGuard<'a, bool>; 2]>,
236}
237
238impl BudgetAdmission<'_> {
239 pub(super) fn try_acquire(&self, bytes: usize, class: BudgetClass) -> Result<ResourcePermit, BudgetRejection> {
240 self.budget.try_acquire_open(bytes, class, true)
241 }
242
243 pub(super) fn try_acquire_waiting(
244 &self,
245 bytes: usize,
246 class: BudgetClass,
247 ) -> Result<ResourcePermit, BudgetRejection> {
248 self.budget.try_acquire_open(bytes, class, false)
249 }
250}
251
252impl fmt::Debug for ResourceBudget {
253 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254 formatter
255 .debug_struct("ResourceBudget")
256 .field("path", &self.node.path)
257 .field("limit", &self.node.limit)
258 .finish()
259 }
260}
261
262impl ResourceBudget {
263 pub fn child(&self, name: impl Into<String>, limit: BudgetLimit) -> Result<Self, crate::RuntimeContractViolation> {
273 self.child_with_gates(name, limit, self.admission_gates.clone())
274 }
275
276 fn child_with_gates(
277 &self,
278 name: impl Into<String>,
279 limit: BudgetLimit,
280 admission_gates: Arc<[Arc<AdmissionGate>]>,
281 ) -> Result<Self, crate::RuntimeContractViolation> {
282 let name = validated_name(name.into())?;
283 let path: Arc<str> = Arc::from(format!("{}/{}", self.node.path, name));
284 limit.validate_child(self.node.limit, &path)?;
285 let node = Arc::new(BudgetNode::new(
286 path,
287 limit,
288 Arc::clone(&self.node.clock),
289 admission_gates.clone(),
290 ));
291 let mut chain = Vec::with_capacity(self.chain.len() + 1);
292 chain.extend(self.chain.iter().cloned());
293 chain.push(Arc::clone(&node));
294 Ok(Self {
295 node,
296 chain: Arc::from(chain),
297 capacity_notify: Arc::clone(&self.capacity_notify),
298 keys: self.keys.clone(),
299 admission_gates,
300 })
301 }
302
303 pub fn register_dynamic_child(
318 &self,
319 name: impl Into<String>,
320 limit: BudgetLimit,
321 ) -> Result<DynamicBudgetKey, DynamicKeyRegistrationFailure> {
322 let _admission = self.admit().map_err(|_| DynamicKeyRegistrationFailure::Closed)?;
323 let name = validated_name(name.into()).map_err(DynamicKeyRegistrationFailure::Invalid)?;
324 let path: Arc<str> = Arc::from(format!("{}/{}", self.node.path, name));
325 let mut gates = self.admission_gates.to_vec();
326 gates.push(Arc::new(AdmissionGate {
327 path: path.clone(),
328 closed: Mutex::new(false),
329 }));
330 let child = self
331 .child_with_gates(name, limit, Arc::from(gates))
332 .map_err(DynamicKeyRegistrationFailure::Invalid)?;
333 self.keys.register(child, path)
334 }
335
336 #[must_use]
339 pub fn is_closed(&self) -> bool {
340 self.admission_gates
341 .iter()
342 .any(|gate| *gate.closed.lock().unwrap_or_else(std::sync::PoisonError::into_inner))
343 }
344
345 pub(super) fn close_dynamic(&self) {
346 if let Some(gate) = self.admission_gates.last() {
347 *gate.closed.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = true;
348 self.capacity_notify.notify_waiters();
349 }
350 }
351
352 pub(super) fn admit(&self) -> Result<BudgetAdmission<'_>, BudgetRejection> {
353 let mut guards = SmallVec::new();
354 for gate in self.admission_gates.iter() {
355 let guard = gate.closed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
356 if *guard {
357 let rejection = BudgetRejection {
358 path: self.node.path.clone(),
359 exhausted_path: gate.path.clone(),
360 reason: BudgetRejectionReason::Closed,
361 policy: self.node.limit.full_policy,
362 };
363 self.record_budget_rejection(&rejection);
364 return Err(rejection);
365 }
366 guards.push(guard);
367 }
368 Ok(BudgetAdmission {
369 budget: self,
370 _guards: guards,
371 })
372 }
373
374 pub fn try_acquire(&self, bytes: usize, class: BudgetClass) -> Result<ResourcePermit, BudgetRejection> {
379 self.try_acquire_internal(bytes, class, true)
380 }
381
382 fn try_acquire_internal(
383 &self,
384 bytes: usize,
385 class: BudgetClass,
386 record_failure: bool,
387 ) -> Result<ResourcePermit, BudgetRejection> {
388 let _admission = self.admit()?;
389 self.try_acquire_open(bytes, class, record_failure)
390 }
391
392 fn try_acquire_open(
393 &self,
394 bytes: usize,
395 class: BudgetClass,
396 record_failure: bool,
397 ) -> Result<ResourcePermit, BudgetRejection> {
398 let mut reservations = SmallVec::<[NodeReservation; 4]>::with_capacity(self.chain.len());
399 for node in self.chain.iter() {
400 match node.try_reserve(bytes, class) {
401 Ok(reservation) => reservations.push(reservation),
402 Err(dimension) => {
403 if record_failure {
404 self.record_node_rejection(node, BudgetRejectionReason::Capacity(dimension));
405 }
406 return Err(BudgetRejection {
407 path: Arc::clone(&self.node.path),
408 exhausted_path: Arc::clone(&node.path),
409 reason: BudgetRejectionReason::Capacity(dimension),
410 policy: self.node.limit.full_policy,
411 });
412 }
413 }
414 }
415 for reservation in &mut reservations {
416 reservation.commit();
417 }
418 Ok(ResourcePermit {
419 reservations,
420 bytes,
421 class,
422 capacity_notify: Arc::clone(&self.capacity_notify),
423 })
424 }
425
426 pub(crate) fn permanent_acquire_rejection(&self, bytes: usize, class: BudgetClass) -> Option<BudgetRejection> {
427 let _admission = match self.admit() {
428 Ok(admission) => admission,
429 Err(rejection) => return Some(rejection),
430 };
431 self.chain.iter().find_map(|node| {
432 let dimension = node.permanent_exhaustion_dimension(bytes, class)?;
433 self.record_node_rejection(node, BudgetRejectionReason::Capacity(dimension));
434 Some(BudgetRejection {
435 path: Arc::clone(&self.node.path),
436 exhausted_path: Arc::clone(&node.path),
437 reason: BudgetRejectionReason::Capacity(dimension),
438 policy: self.node.limit.full_policy,
439 })
440 })
441 }
442
443 pub(crate) fn record_budget_rejection(&self, error: &BudgetRejection) {
444 if let Some(node) = self
445 .chain
446 .iter()
447 .find(|node| node.path.as_ref() == error.exhausted_path())
448 {
449 self.record_node_rejection(node, error.reason());
450 }
451 }
452
453 fn record_node_rejection(&self, exhausted_node: &Arc<BudgetNode>, reason: BudgetRejectionReason) {
454 exhausted_node.record_rejection(reason);
455 if !Arc::ptr_eq(exhausted_node, &self.node) {
456 self.node.record_rejection(reason);
457 }
458 }
459
460 pub(crate) fn capacity_notify(&self) -> &Notify {
461 &self.capacity_notify
462 }
463
464 pub fn try_acquire_data(&self, bytes: usize) -> Result<ResourcePermit, BudgetRejection> {
469 self.try_acquire(bytes, BudgetClass::Data)
470 }
471
472 pub fn try_acquire_control(&self, bytes: usize) -> Result<ResourcePermit, BudgetRejection> {
477 self.try_acquire(bytes, BudgetClass::Control)
478 }
479
480 #[must_use]
481 pub fn snapshot(&self) -> BudgetSnapshot {
483 self.node.snapshot()
484 }
485
486 #[must_use]
487 pub fn path(&self) -> &str {
489 &self.node.path
490 }
491
492 #[must_use]
493 pub fn limit(&self) -> BudgetLimit {
495 self.node.limit
496 }
497
498 #[must_use]
500 pub fn monotonic_now(&self) -> Duration {
501 self.node.clock.now()
502 }
503
504 pub fn record_dropped(&self, count: usize) {
506 self.node.record_dropped(count);
507 }
508
509 pub fn record_coalesced(&self, count: usize) {
511 self.node.record_coalesced(count);
512 }
513
514 pub fn record_slow_consumer_closed(&self) {
516 self.node.record_slow_consumer_closed();
517 }
518}
519
520fn validated_name(name: String) -> Result<String, crate::RuntimeContractViolation> {
521 let name = name.trim();
522 if name.is_empty() {
523 return Err(crate::RuntimeContractViolation::EmptyBudgetName);
524 }
525 if name.contains('/') {
526 return Err(crate::RuntimeContractViolation::InvalidBudgetName);
527 }
528 Ok(name.to_owned())
529}
530
531pub struct ResourcePermit {
533 reservations: SmallVec<[NodeReservation; 4]>,
534 bytes: usize,
535 class: BudgetClass,
536 capacity_notify: Arc<Notify>,
537}
538
539impl fmt::Debug for ResourcePermit {
540 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
541 formatter
542 .debug_struct("ResourcePermit")
543 .field(
544 "path",
545 &self.reservations.last().map(|reservation| &reservation.node.path),
546 )
547 .field("bytes", &self.bytes)
548 .field("class", &self.class)
549 .finish()
550 }
551}
552
553impl ResourcePermit {
554 pub fn try_resize(&mut self, bytes: usize) -> Result<(), BudgetRejection> {
566 if bytes <= self.bytes {
567 self.shrink_to(bytes);
568 return Ok(());
569 }
570 let leaf = Arc::clone(&self.reservations.last().expect("permits own a root reservation").node);
572 let mut guards = SmallVec::<[MutexGuard<'_, bool>; 2]>::new();
573 for gate in leaf.admission_gates.iter() {
574 let guard = gate.closed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
575 if *guard {
576 leaf.record_rejection(BudgetRejectionReason::Closed);
577 return Err(BudgetRejection {
578 path: leaf.path.clone(),
579 exhausted_path: gate.path.clone(),
580 reason: BudgetRejectionReason::Closed,
581 policy: leaf.limit.full_policy,
582 });
583 }
584 guards.push(guard);
585 }
586 let additional = bytes - self.bytes;
587 for (index, reservation) in self.reservations.iter().enumerate() {
588 if !reservation.node.try_reserve_bytes(additional, self.class) {
589 for reserved in &self.reservations[..index] {
590 reserved.node.release_bytes(additional, self.class);
591 }
592 let reason = BudgetRejectionReason::Capacity(BudgetDimension::Bytes);
593 reservation.node.record_rejection(reason);
594 if !Arc::ptr_eq(&reservation.node, &leaf) {
595 leaf.record_rejection(reason);
596 }
597 self.capacity_notify.notify_waiters();
598 return Err(BudgetRejection {
599 path: leaf.path.clone(),
600 exhausted_path: reservation.node.path.clone(),
601 reason,
602 policy: leaf.limit.full_policy,
603 });
604 }
605 }
606 for reservation in &mut self.reservations {
607 reservation.bytes = bytes;
608 }
609 self.bytes = bytes;
610 Ok(())
611 }
612
613 pub(crate) fn shrink_to(&mut self, bytes: usize) {
614 let released = self.bytes.saturating_sub(bytes);
615 if released == 0 {
616 return;
617 }
618 for reservation in &mut self.reservations {
619 reservation.node.release_bytes(released, self.class);
620 reservation.bytes = bytes;
621 }
622 self.bytes = bytes;
623 self.capacity_notify.notify_waiters();
624 }
625
626 #[must_use]
627 pub const fn bytes(&self) -> usize {
629 self.bytes
630 }
631
632 #[must_use]
633 pub const fn class(&self) -> BudgetClass {
635 self.class
636 }
637
638 pub fn promote_to_control(&mut self) {
643 if self.class == BudgetClass::Control {
644 return;
645 }
646 for reservation in &mut self.reservations {
647 let node = &reservation.node;
648 release_reserved(&node.data_count, 1);
649 release_reserved(&node.data_bytes, reservation.bytes);
650 if let Some(rate) = &node.rate {
651 BudgetNode::lock_rate(rate).restore_data(node.limit);
652 }
653 reservation.class = BudgetClass::Control;
654 }
655 self.class = BudgetClass::Control;
656 self.capacity_notify.notify_waiters();
657 }
658
659 pub fn try_rebind(
674 &mut self,
675 target: &ResourceBudget,
676 ) -> Result<PermitRebindOutcome, crate::RuntimeContractViolation> {
677 if !self.belongs_to_tree(target) {
678 return Err(crate::RuntimeContractViolation::PermitTargetInDifferentTree);
679 }
680 let admission = match target.admit() {
681 Ok(admission) => admission,
682 Err(rejection) => return Ok(PermitRebindOutcome::Rejected(rejection)),
683 };
684 Ok(self.try_rebind_admitted(&admission))
685 }
686
687 pub(super) fn belongs_to_tree(&self, target: &ResourceBudget) -> bool {
688 self.reservations
689 .first()
690 .zip(target.chain.first())
691 .is_some_and(|(reservation, node)| Arc::ptr_eq(&reservation.node, node))
692 }
693
694 pub(super) fn try_rebind_admitted(&mut self, admission: &BudgetAdmission<'_>) -> PermitRebindOutcome {
695 let target = admission.budget;
696 let common_ancestors = self
697 .reservations
698 .iter()
699 .zip(target.chain.iter())
700 .take_while(|(reservation, target_node)| Arc::ptr_eq(&reservation.node, target_node))
701 .count();
702 debug_assert!(
703 common_ancestors > 0,
704 "caller validates the permit tree before admission"
705 );
706 if common_ancestors == self.reservations.len() && common_ancestors == target.chain.len() {
707 return PermitRebindOutcome::Unchanged;
708 }
709
710 let mut target_reservations =
711 SmallVec::<[NodeReservation; 4]>::with_capacity(target.chain.len() - common_ancestors);
712 for node in target.chain.iter().skip(common_ancestors) {
713 match node.try_reserve(self.bytes, self.class) {
714 Ok(reservation) => target_reservations.push(reservation),
715 Err(dimension) => {
716 target.record_node_rejection(node, BudgetRejectionReason::Capacity(dimension));
717 return PermitRebindOutcome::Rejected(BudgetRejection {
718 path: Arc::clone(&target.node.path),
719 exhausted_path: Arc::clone(&node.path),
720 reason: BudgetRejectionReason::Capacity(dimension),
721 policy: target.node.limit.full_policy,
722 });
723 }
724 }
725 }
726 for reservation in &mut target_reservations {
727 reservation.commit();
728 }
729
730 self.reservations.truncate(common_ancestors);
731 self.reservations.extend(target_reservations);
732 self.capacity_notify.notify_waiters();
733 PermitRebindOutcome::Rebound
734 }
735}
736
737impl Drop for ResourcePermit {
738 fn drop(&mut self) {
739 self.reservations.clear();
740 self.capacity_notify.notify_waiters();
741 }
742}
743
744struct BudgetNode {
754 path: Arc<str>,
755 limit: BudgetLimit,
756 clock: Arc<dyn MonotonicClock>,
757 current_count: AtomicUsize,
758 current_bytes: AtomicUsize,
759 data_count: AtomicUsize,
760 data_bytes: AtomicUsize,
761 counters: BudgetCounters,
762 rate: Option<Mutex<RateState>>,
763 admission_gates: Arc<[Arc<AdmissionGate>]>,
766}
767
768#[derive(Default)]
769struct BudgetCounters {
770 admitted: AtomicU64,
771 released: AtomicU64,
772 rejected: AtomicU64,
773 throttled: AtomicU64,
774 dropped: AtomicU64,
775 coalesced: AtomicU64,
776 closed_slow_consumer: AtomicU64,
777}
778
779fn try_reserve_within(counter: &AtomicUsize, amount: usize, limit: usize) -> bool {
781 counter
782 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
783 current.checked_add(amount).filter(|total| *total <= limit)
784 })
785 .is_ok()
786}
787
788fn release_reserved(counter: &AtomicUsize, amount: usize) {
789 let previous = counter.fetch_sub(amount, Ordering::AcqRel);
790 debug_assert!(previous >= amount, "budget capacity released more than reserved");
791}
792
793impl BudgetNode {
794 fn try_reserve_bytes(&self, bytes: usize, class: BudgetClass) -> bool {
795 if !try_reserve_within(&self.current_bytes, bytes, self.limit.capacity.bytes) {
796 return false;
797 }
798 if class == BudgetClass::Data
799 && !try_reserve_within(
800 &self.data_bytes,
801 bytes,
802 self.limit
803 .capacity
804 .bytes
805 .saturating_sub(self.limit.control_reserve.bytes),
806 )
807 {
808 release_reserved(&self.current_bytes, bytes);
809 return false;
810 }
811 true
812 }
813
814 fn release_bytes(&self, bytes: usize, class: BudgetClass) {
815 if class == BudgetClass::Data {
816 release_reserved(&self.data_bytes, bytes);
817 }
818 release_reserved(&self.current_bytes, bytes);
819 }
820
821 fn new(
822 path: Arc<str>,
823 limit: BudgetLimit,
824 clock: Arc<dyn MonotonicClock>,
825 admission_gates: Arc<[Arc<AdmissionGate>]>,
826 ) -> Self {
827 let rate = limit
828 .capacity
829 .rate
830 .map(|_| Mutex::new(RateState::new(limit, clock.now())));
831 Self {
832 path,
833 limit,
834 clock,
835 current_count: AtomicUsize::new(0),
836 current_bytes: AtomicUsize::new(0),
837 data_count: AtomicUsize::new(0),
838 data_bytes: AtomicUsize::new(0),
839 counters: BudgetCounters::default(),
840 rate,
841 admission_gates,
842 }
843 }
844
845 fn lock_rate(rate: &Mutex<RateState>) -> MutexGuard<'_, RateState> {
846 rate.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
847 }
848
849 fn try_reserve(self: &Arc<Self>, bytes: usize, class: BudgetClass) -> Result<NodeReservation, BudgetDimension> {
850 let capacity = self.limit.capacity;
851 let control_reserve = self.limit.control_reserve;
852 if !try_reserve_within(&self.current_count, 1, capacity.count) {
853 return Err(BudgetDimension::Count);
854 }
855 if !try_reserve_within(&self.current_bytes, bytes, capacity.bytes) {
856 release_reserved(&self.current_count, 1);
857 return Err(BudgetDimension::Bytes);
858 }
859 if class == BudgetClass::Data {
860 if !try_reserve_within(
861 &self.data_count,
862 1,
863 capacity.count.saturating_sub(control_reserve.count),
864 ) {
865 self.release_capacity(bytes, BudgetClass::Control);
866 return Err(BudgetDimension::Count);
867 }
868 if !try_reserve_within(
869 &self.data_bytes,
870 bytes,
871 capacity.bytes.saturating_sub(control_reserve.bytes),
872 ) {
873 release_reserved(&self.data_count, 1);
874 self.release_capacity(bytes, BudgetClass::Control);
875 return Err(BudgetDimension::Bytes);
876 }
877 }
878 if let Some(rate) = &self.rate {
879 let mut rate = Self::lock_rate(rate);
880 rate.refill(self.limit, self.clock.now());
881 if !rate.available(self.limit, class) {
882 drop(rate);
883 self.release_capacity(bytes, class);
884 return Err(BudgetDimension::Rate);
885 }
886 rate.consume(self.limit, class);
887 }
888
889 Ok(NodeReservation {
890 node: Arc::clone(self),
891 bytes,
892 class,
893 committed: false,
894 })
895 }
896
897 fn release_capacity(&self, bytes: usize, class: BudgetClass) {
899 if class == BudgetClass::Data {
900 release_reserved(&self.data_count, 1);
901 release_reserved(&self.data_bytes, bytes);
902 }
903 release_reserved(&self.current_bytes, bytes);
904 release_reserved(&self.current_count, 1);
905 }
906
907 fn permanent_exhaustion_dimension(&self, bytes: usize, class: BudgetClass) -> Option<BudgetDimension> {
908 let (count_capacity, byte_capacity) = match class {
909 BudgetClass::Data => (
910 self.limit
911 .capacity
912 .count
913 .saturating_sub(self.limit.control_reserve.count),
914 self.limit
915 .capacity
916 .bytes
917 .saturating_sub(self.limit.control_reserve.bytes),
918 ),
919 BudgetClass::Control => (self.limit.capacity.count, self.limit.capacity.bytes),
920 };
921 if count_capacity == 0 {
922 Some(BudgetDimension::Count)
923 } else if bytes > byte_capacity {
924 Some(BudgetDimension::Bytes)
925 } else {
926 None
927 }
928 }
929
930 fn record_rejection(&self, reason: BudgetRejectionReason) {
931 self.counters.rejected.fetch_add(1, Ordering::Relaxed);
932 if reason == BudgetRejectionReason::Capacity(BudgetDimension::Rate) {
933 self.counters.throttled.fetch_add(1, Ordering::Relaxed);
934 }
935 }
936
937 fn record_dropped(&self, count: usize) {
938 self.counters.dropped.fetch_add(count as u64, Ordering::Relaxed);
939 }
940
941 fn record_coalesced(&self, count: usize) {
942 self.counters.coalesced.fetch_add(count as u64, Ordering::Relaxed);
943 }
944
945 fn record_slow_consumer_closed(&self) {
946 self.counters.closed_slow_consumer.fetch_add(1, Ordering::Relaxed);
947 }
948
949 fn snapshot(&self) -> BudgetSnapshot {
951 BudgetSnapshot {
952 path: Arc::clone(&self.path),
953 current_count: self.current_count.load(Ordering::Acquire),
954 current_bytes: self.current_bytes.load(Ordering::Acquire),
955 admitted_count: self.counters.admitted.load(Ordering::Relaxed),
956 released_count: self.counters.released.load(Ordering::Relaxed),
957 rejected_count: self.counters.rejected.load(Ordering::Relaxed),
958 throttled_count: self.counters.throttled.load(Ordering::Relaxed),
959 dropped_count: self.counters.dropped.load(Ordering::Relaxed),
960 coalesced_count: self.counters.coalesced.load(Ordering::Relaxed),
961 closed_slow_consumer_count: self.counters.closed_slow_consumer.load(Ordering::Relaxed),
962 }
963 }
964}
965
966struct NodeReservation {
967 node: Arc<BudgetNode>,
968 bytes: usize,
969 class: BudgetClass,
970 committed: bool,
971}
972
973impl NodeReservation {
974 fn commit(&mut self) {
975 self.node.counters.admitted.fetch_add(1, Ordering::Relaxed);
976 self.committed = true;
977 }
978}
979
980impl Drop for NodeReservation {
981 fn drop(&mut self) {
982 self.node.release_capacity(self.bytes, self.class);
983 if self.committed {
984 self.node.counters.released.fetch_add(1, Ordering::Relaxed);
985 } else if let Some(rate) = &self.node.rate {
986 BudgetNode::lock_rate(rate).restore(self.node.limit, self.class);
988 }
989 }
990}
991
992struct RateState {
994 total_tokens: Option<TokenBucket>,
995 data_tokens: Option<TokenBucket>,
996}
997
998impl RateState {
999 fn new(limit: BudgetLimit, now: Duration) -> Self {
1000 Self {
1001 total_tokens: limit.capacity.rate.map(|rate| TokenBucket::new(rate, now)),
1002 data_tokens: data_rate_limit(limit).map(|rate| TokenBucket::new(rate, now)),
1003 }
1004 }
1005
1006 fn refill(&mut self, limit: BudgetLimit, now: Duration) {
1007 if let (Some(bucket), Some(rate)) = (&mut self.total_tokens, limit.capacity.rate) {
1008 bucket.refill(rate, now);
1009 }
1010 if let (Some(bucket), Some(rate)) = (&mut self.data_tokens, data_rate_limit(limit)) {
1011 bucket.refill(rate, now);
1012 }
1013 }
1014
1015 fn available(&self, limit: BudgetLimit, class: BudgetClass) -> bool {
1016 let total_available = self.total_tokens.as_ref().is_none_or(TokenBucket::has_token);
1017 let data_available = class == BudgetClass::Control
1018 || data_rate_limit(limit).is_none()
1019 || self.data_tokens.as_ref().is_some_and(TokenBucket::has_token);
1020 total_available && data_available
1021 }
1022
1023 fn consume(&mut self, limit: BudgetLimit, class: BudgetClass) {
1024 if let Some(bucket) = &mut self.total_tokens {
1025 bucket.consume();
1026 }
1027 if class == BudgetClass::Data && data_rate_limit(limit).is_some() {
1028 if let Some(bucket) = &mut self.data_tokens {
1029 bucket.consume();
1030 }
1031 }
1032 }
1033
1034 fn restore(&mut self, limit: BudgetLimit, class: BudgetClass) {
1035 if let (Some(bucket), Some(rate)) = (&mut self.total_tokens, limit.capacity.rate) {
1036 bucket.restore(rate);
1037 }
1038 if class == BudgetClass::Data {
1039 self.restore_data(limit);
1040 }
1041 }
1042
1043 fn restore_data(&mut self, limit: BudgetLimit) {
1044 if let (Some(bucket), Some(rate)) = (&mut self.data_tokens, data_rate_limit(limit)) {
1045 bucket.restore(rate);
1046 }
1047 }
1048}
1049
1050fn data_rate_limit(limit: BudgetLimit) -> Option<RateLimit> {
1051 let total = limit.capacity.rate?;
1052 let reserve = limit.control_reserve.rate.unwrap_or(RateLimit::new(0, 0));
1053 Some(RateLimit::new(
1054 total.permits_per_second.saturating_sub(reserve.permits_per_second),
1055 total.burst.saturating_sub(reserve.burst),
1056 ))
1057}
1058
1059struct TokenBucket {
1060 tokens: f64,
1061 last_refill: Duration,
1062}
1063
1064impl TokenBucket {
1065 fn new(rate: RateLimit, now: Duration) -> Self {
1066 Self {
1067 tokens: rate.burst as f64,
1068 last_refill: now,
1069 }
1070 }
1071
1072 fn refill(&mut self, rate: RateLimit, now: Duration) {
1073 let elapsed = now.saturating_sub(self.last_refill).as_secs_f64();
1074 self.tokens = (self.tokens + elapsed * rate.permits_per_second as f64).min(rate.burst as f64);
1075 self.last_refill = now;
1076 }
1077
1078 fn has_token(&self) -> bool {
1079 self.tokens >= 1.0
1080 }
1081
1082 fn consume(&mut self) {
1083 self.tokens = (self.tokens - 1.0).max(0.0);
1084 }
1085
1086 fn restore(&mut self, rate: RateLimit) {
1087 self.tokens = (self.tokens + 1.0).min(rate.burst as f64);
1088 }
1089}