Skip to main content

rocketmq_runtime/resource_budget/
budget.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Represents budget snapshot.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BudgetSnapshot {
41    /// The path value.
42    pub path: Arc<str>,
43    /// The number of current entries.
44    pub current_count: usize,
45    /// The current size in bytes.
46    pub current_bytes: usize,
47    /// The number of admitted entries.
48    pub admitted_count: u64,
49    /// The number of released entries.
50    pub released_count: u64,
51    /// The number of rejected entries.
52    pub rejected_count: u64,
53    /// The number of throttled entries.
54    pub throttled_count: u64,
55    /// The number of dropped entries.
56    pub dropped_count: u64,
57    /// The number of coalesced entries.
58    pub coalesced_count: u64,
59    /// The number of closed slow consumer entries.
60    pub closed_slow_consumer_count: u64,
61}
62
63/// A normal resource-budget admission rejection.
64#[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/// Why a resource budget refused new work.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum BudgetRejectionReason {
75    /// Count, byte, or rate capacity is exhausted.
76    Capacity(BudgetDimension),
77    /// This dynamic generation or one of its ancestors has closed admission.
78    Closed,
79}
80
81impl BudgetRejection {
82    #[must_use]
83    /// Returns the path.
84    pub fn path(&self) -> &str {
85        &self.path
86    }
87
88    #[must_use]
89    /// Returns the exhausted path.
90    pub fn exhausted_path(&self) -> &str {
91        &self.exhausted_path
92    }
93
94    #[must_use]
95    /// Returns the exhausted capacity dimension, or `None` for closed admission.
96    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    /// Returns the admission rejection reason without projecting closure onto capacity.
104    #[must_use]
105    pub const fn reason(&self) -> BudgetRejectionReason {
106        self.reason
107    }
108
109    /// Returns whether admission has permanently closed for this handle.
110    #[must_use]
111    pub const fn is_closed(&self) -> bool {
112        matches!(self.reason, BudgetRejectionReason::Closed)
113    }
114
115    #[must_use]
116    /// Returns the policy.
117    pub const fn policy(&self) -> FullPolicy {
118        self.policy
119    }
120}
121
122/// The result of an attempted permit rebind.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum PermitRebindOutcome {
125    /// The permit now owns the target budget chain.
126    Rebound,
127    /// The permit was already bound to the requested target.
128    Unchanged,
129    /// The target rejected the permit and the source permit is unchanged.
130    Rejected(BudgetRejection),
131}
132
133/// Represents resource budget tree.
134pub 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    /// Creates a new `ResourceBudgetTree`.
149    ///
150    /// # Errors
151    ///
152    /// Returns a contract violation when the root name is blank or malformed,
153    /// or when its budget limit is invalid.
154    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    /// Creates a tree with an injected monotonic clock.
159    ///
160    /// # Errors
161    ///
162    /// Returns a contract violation when the root name is blank or malformed,
163    /// or when its budget limit is invalid.
164    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    /// Creates a tree whose dynamic key registry holds at most `max_entries`
191    /// names.
192    ///
193    /// # Errors
194    ///
195    /// Returns a contract violation when the root name is blank or malformed,
196    /// or when its budget limit is invalid.
197    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    /// Returns the root.
210    pub fn root(&self) -> ResourceBudget {
211        self.root.clone()
212    }
213}
214
215/// Represents resource budget.
216#[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
230// Always acquire gates root-to-leaf, before node or queue state locks. Static
231// budgets have no gates. Keeping this guard across a synchronous queue admission
232// also prevents closure from triggering destructive capacity/age policies.
233pub(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    /// Creates a child that inherits every dynamic ancestor's admission state.
264    ///
265    /// Creating a static child after closure is allowed; the resulting handle
266    /// remains closed and cannot acquire or accept rebound permits.
267    ///
268    /// # Errors
269    ///
270    /// Returns a contract violation when the child name is blank or malformed,
271    /// its limit is invalid, or its limit exceeds its parent.
272    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    /// Registers a bounded dynamic child key under this budget.
304    ///
305    /// The key owns a child budget and holds its name reserved until
306    /// [`DynamicBudgetKey::retire_until`] observes that the child has no
307    /// reservations left. A name that is already held cannot be registered
308    /// again, so a retired generation never runs beside the one that replaced
309    /// it. Use [`Self::child`] when the key does not need a retirement
310    /// lifecycle.
311    ///
312    /// # Errors
313    ///
314    /// Returns [`DynamicKeyRegistrationFailure`] when the child contract is
315    /// invalid, a dynamic ancestor has closed, the name is still held, or the
316    /// tree holds its maximum number of dynamic keys.
317    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    /// Returns whether a dynamic ancestor has permanently closed admission.
337    /// Existing permits remain valid and can still be released or moved to an open budget.
338    #[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    /// Attempts to acquire.
375    ///
376    /// A [`BudgetRejection`] is a normal bounded outcome when the requested
377    /// capacity is unavailable; it is not an operational runtime failure.
378    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    /// Attempts to acquire data.
465    ///
466    /// A [`BudgetRejection`] is a normal bounded outcome when data capacity is
467    /// unavailable; it is not an operational runtime failure.
468    pub fn try_acquire_data(&self, bytes: usize) -> Result<ResourcePermit, BudgetRejection> {
469        self.try_acquire(bytes, BudgetClass::Data)
470    }
471
472    /// Attempts to acquire control.
473    ///
474    /// A [`BudgetRejection`] is a normal bounded outcome when control capacity
475    /// is unavailable; it is not an operational runtime failure.
476    pub fn try_acquire_control(&self, bytes: usize) -> Result<ResourcePermit, BudgetRejection> {
477        self.try_acquire(bytes, BudgetClass::Control)
478    }
479
480    #[must_use]
481    /// Returns the snapshot.
482    pub fn snapshot(&self) -> BudgetSnapshot {
483        self.node.snapshot()
484    }
485
486    #[must_use]
487    /// Returns the path.
488    pub fn path(&self) -> &str {
489        &self.node.path
490    }
491
492    #[must_use]
493    /// Returns the limit.
494    pub fn limit(&self) -> BudgetLimit {
495        self.node.limit
496    }
497
498    /// Returns the current instant from this tree's injected monotonic clock.
499    #[must_use]
500    pub fn monotonic_now(&self) -> Duration {
501        self.node.clock.now()
502    }
503
504    /// Records items discarded by a queue or custom retention boundary.
505    pub fn record_dropped(&self, count: usize) {
506        self.node.record_dropped(count);
507    }
508
509    /// Records items folded into an already retained logical item.
510    pub fn record_coalesced(&self, count: usize) {
511        self.node.record_coalesced(count);
512    }
513
514    /// Records a slow consumer closed by this budget's full policy.
515    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
531/// Represents resource permit.
532pub 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    /// Changes the byte reservation without acquiring another count or rate token.
555    ///
556    /// Growth reserves only the difference at each ancestor. A rejection rolls
557    /// back every added byte and leaves this permit unchanged. Shrinking is
558    /// allowed after budget closure; callers release the removed payload before
559    /// shrinking its charge. This method does not block on available capacity.
560    ///
561    /// # Errors
562    ///
563    /// Returns a rejection if growth exceeds any ancestor's byte limit or a
564    /// dynamic ancestor has closed admission.
565    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        // Every live permit owns at least its root reservation.
571        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    /// Returns the bytes.
628    pub const fn bytes(&self) -> usize {
629        self.bytes
630    }
631
632    #[must_use]
633    /// Returns the class.
634    pub const fn class(&self) -> BudgetClass {
635        self.class
636    }
637
638    /// Promotes a data reservation to the control class without releasing capacity.
639    ///
640    /// The total count and byte ownership remain unchanged at every budget level. Data-only
641    /// counters and rate tokens are released before a same-tree rebind can use control reserve.
642    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    /// Moves this reservation to another budget in the same resource tree.
660    ///
661    /// Reservations for the common ancestor chain remain owned throughout the
662    /// transfer. Target-only reservations are acquired before source-only
663    /// reservations are released, so the payload is always covered without
664    /// charging common ancestors twice.
665    ///
666    /// Returns a normal rejection when the target is full. Contract failures
667    /// and rejections leave this permit unchanged and valid for its source.
668    ///
669    /// # Errors
670    ///
671    /// Returns [`crate::RuntimeContractViolation::PermitTargetInDifferentTree`]
672    /// when `target` is outside this permit's resource-budget tree.
673    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
744/// One budget level.
745///
746/// Count and byte capacity is reserved with atomic read-modify-writes. A
747/// request that fails at a later dimension, or at a later node of its chain,
748/// rolls back what it reserved, so no counter ever exceeds its limit. A
749/// concurrent request that observes capacity during such a rollback can be
750/// rejected even though the capacity is about to return.
751///
752/// Only a node that configures a rate takes a lock, around its token buckets.
753struct 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    // Resize uses the leaf node's gates, including after rebind. Keep this
764    // shared state out of every ResourcePermit and its downstream containers.
765    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
779/// Adds `amount` to `counter` when the sum stays within `limit`.
780fn 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    /// Returns reserved count and bytes; data counters too for a data reservation.
898    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    /// Reads each counter atomically; the fields are not one consistent cut.
950    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            // A rolled-back reservation returns the rate token it consumed.
987            BudgetNode::lock_rate(rate).restore(self.node.limit, self.class);
988        }
989    }
990}
991
992/// Token buckets of a rate-limited node.
993struct 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}