Skip to main content

rocketmq_runtime/resource_budget/
queue.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::collections::VecDeque;
16use std::fmt;
17use std::sync::atomic::AtomicU64;
18use std::sync::atomic::AtomicUsize;
19use std::sync::atomic::Ordering;
20use std::sync::Arc;
21use std::sync::Mutex;
22use std::time::Duration;
23
24use tokio::sync::Notify;
25use tokio::time::Instant;
26
27use super::budget::BudgetAdmission;
28use super::budget::BudgetRejection;
29use super::budget::PermitRebindOutcome;
30use super::budget::ResourceBudget;
31use super::budget::ResourcePermit;
32use super::limit::BudgetClass;
33use super::limit::BudgetDimension;
34use super::limit::FullPolicy;
35
36/// Identifies the queue push outcome state.
37#[derive(Debug)]
38pub enum QueuePushOutcome<T> {
39    /// Represents the enqueued case.
40    Enqueued,
41    /// Represents the coalesced case.
42    Coalesced {
43        /// The replaced value.
44        replaced: usize,
45    },
46    /// Represents the dropped stale case.
47    DroppedStale {
48        /// The dropped value.
49        dropped: usize,
50    },
51    /// The item was not accepted and is returned to the caller unchanged.
52    Rejected {
53        /// The submitted item.
54        item: T,
55        /// The source-free normal rejection reason.
56        rejection: QueuePushRejection,
57    },
58}
59
60/// A source-free normal queue-admission rejection.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum QueuePushRejection {
63    /// The resource budget rejected the item.
64    BudgetExhausted(BudgetRejection),
65    /// The absolute admission deadline elapsed.
66    DeadlineExceeded,
67    /// The queue is closed.
68    Closed,
69    /// The queue closed a slow consumer.
70    SlowConsumerClosed,
71}
72
73/// A push rejected because its permit belongs to another budget tree.
74///
75/// The item and its unchanged permit are returned together. The permit still
76/// holds the charge the caller acquired, so dropping it here would release
77/// capacity that the caller is still accounting for.
78///
79/// The permit is boxed because a [`ResourcePermit`] inlines its ancestor
80/// reservations and this is only reachable when a caller passes a permit from
81/// an unrelated budget tree.
82#[derive(Debug)]
83pub struct ForeignPermit<T> {
84    /// The item that was not enqueued.
85    pub item: T,
86    /// The unchanged permit, still holding its charge.
87    pub permit: Box<ResourcePermit>,
88}
89
90/// Represents budgeted item.
91pub struct BudgetedItem<T> {
92    item: T,
93    enqueued_at: Duration,
94    permit: ResourcePermit,
95}
96
97impl<T> BudgetedItem<T> {
98    #[must_use]
99    /// Converts this value into item.
100    pub fn into_item(self) -> T {
101        self.item
102    }
103
104    #[must_use]
105    /// Converts this value into parts.
106    pub fn into_parts(self) -> (T, ResourcePermit, Duration) {
107        (self.item, self.permit, self.enqueued_at)
108    }
109
110    #[must_use]
111    /// Returns the enqueued at.
112    pub const fn enqueued_at(&self) -> Duration {
113        self.enqueued_at
114    }
115
116    #[must_use]
117    /// Returns the retained bytes.
118    pub fn retained_bytes(&self) -> usize {
119        self.permit.bytes()
120    }
121}
122
123/// Represents queue snapshot.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct QueueSnapshot {
126    /// The path value.
127    pub path: Arc<str>,
128    /// The depth value.
129    pub depth: usize,
130    /// The number of reserved entries.
131    pub reserved_count: usize,
132    /// The retained size in bytes.
133    pub retained_bytes: usize,
134    /// The oldest age value.
135    pub oldest_age: Option<Duration>,
136    /// The number of throttled entries.
137    pub throttled_count: u64,
138    /// The number of rejected entries.
139    pub rejected_count: u64,
140    /// The number of dropped entries.
141    pub dropped_count: u64,
142    /// The number of coalesced entries.
143    pub coalesced_count: u64,
144    /// The number of closed slow consumer entries.
145    pub closed_slow_consumer_count: u64,
146    /// The number of producers currently waiting for admission.
147    pub waiters: usize,
148    /// The cumulative number of producers that waited for admission.
149    pub wait_count: u64,
150    /// The number of items returned because their admission deadline elapsed.
151    pub deadline_exceeded_count: u64,
152    /// Whether closed.
153    pub closed: bool,
154}
155
156/// Represents budgeted queue.
157pub struct BudgetedQueue<T> {
158    inner: Arc<QueueInner<T>>,
159}
160
161impl<T> Clone for BudgetedQueue<T> {
162    fn clone(&self) -> Self {
163        Self {
164            inner: Arc::clone(&self.inner),
165        }
166    }
167}
168
169impl<T> fmt::Debug for BudgetedQueue<T> {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        formatter
172            .debug_struct("BudgetedQueue")
173            .field("budget", &self.inner.budget)
174            .finish_non_exhaustive()
175    }
176}
177
178impl<T> BudgetedQueue<T> {
179    #[must_use]
180    /// Creates a new `BudgetedQueue`.
181    pub fn new(budget: ResourceBudget) -> Self {
182        Self {
183            inner: Arc::new(QueueInner {
184                budget,
185                state: Mutex::new(QueueState {
186                    items: VecDeque::new(),
187                    closed: false,
188                }),
189                coalesce_push: Mutex::new(()),
190                notify: Notify::new(),
191                metrics: QueueMetrics::default(),
192            }),
193        }
194    }
195
196    /// Returns whether two handles refer to the same physical queue.
197    #[must_use]
198    pub fn is_same_queue(&self, other: &Self) -> bool {
199        Arc::ptr_eq(&self.inner, &other.inner)
200    }
201
202    /// Attempts to push.
203    pub fn try_push(&self, item: T, retained_bytes: usize, class: BudgetClass) -> QueuePushOutcome<T> {
204        // Retained permits are released inside admission; arbitrary item
205        // destructors run only after every queue and dynamic gate is unlocked.
206        let mut discarded = Vec::new();
207        let _coalesce_guard = if self.inner.budget.limit().full_policy == FullPolicy::CoalesceLatest {
208            Some(
209                self.inner
210                    .coalesce_push
211                    .lock()
212                    .unwrap_or_else(std::sync::PoisonError::into_inner),
213            )
214        } else {
215            None
216        };
217        let admission = match self.inner.budget.admit() {
218            Ok(admission) => admission,
219            Err(_) => {
220                return QueuePushOutcome::Rejected {
221                    item,
222                    rejection: QueuePushRejection::Closed,
223                }
224            }
225        };
226        let dropped = self.apply_age_policy(&mut discarded);
227        match admission.try_acquire(retained_bytes, class) {
228            Ok(permit) => {
229                if let Some(rejected) = self.enqueue(item, permit) {
230                    return rejected;
231                }
232                if dropped == 0 {
233                    QueuePushOutcome::Enqueued
234                } else {
235                    QueuePushOutcome::DroppedStale { dropped }
236                }
237            }
238            Err(error) => self.handle_full(&admission, item, retained_bytes, class, error, &mut discarded),
239        }
240    }
241
242    /// Pushes an item whose retained bytes are already charged by `permit`.
243    ///
244    /// The permit is transferred into the queue, so an admission at the
245    /// caller's own ingress boundary and the queue retention charge the shared
246    /// ancestor chain exactly once. A permit acquired from an ancestor of this
247    /// queue's budget is rebound to the queue before it is retained, which is
248    /// the difference between this entry point and the ones that acquire their
249    /// own permit from `retained_bytes`.
250    ///
251    /// A rejection returns the item and releases the transferred charge, which
252    /// matches the entry points that acquire their own permit: an item that was
253    /// not retained holds no reservation.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`ForeignPermit`] with the item and its unchanged permit when
258    /// `permit` does not belong to this queue's budget tree. The permit still
259    /// holds the caller's charge, so it is handed back instead of dropped.
260    pub fn try_push_budgeted(
261        &self,
262        item: T,
263        mut permit: ResourcePermit,
264    ) -> Result<QueuePushOutcome<T>, ForeignPermit<T>> {
265        let mut discarded = Vec::new();
266        if !permit.belongs_to_tree(&self.inner.budget) {
267            return Err(ForeignPermit {
268                item,
269                permit: Box::new(permit),
270            });
271        }
272        let _coalesce_guard = if self.inner.budget.limit().full_policy == FullPolicy::CoalesceLatest {
273            Some(
274                self.inner
275                    .coalesce_push
276                    .lock()
277                    .unwrap_or_else(std::sync::PoisonError::into_inner),
278            )
279        } else {
280            None
281        };
282        let admission = match self.inner.budget.admit() {
283            Ok(admission) => admission,
284            Err(_) => {
285                return Ok(QueuePushOutcome::Rejected {
286                    item,
287                    rejection: QueuePushRejection::Closed,
288                })
289            }
290        };
291        let dropped = self.apply_age_policy(&mut discarded);
292        match permit.try_rebind_admitted(&admission) {
293            PermitRebindOutcome::Rebound | PermitRebindOutcome::Unchanged => {
294                if let Some(rejected) = self.enqueue(item, permit) {
295                    return Ok(rejected);
296                }
297                Ok(if dropped == 0 {
298                    QueuePushOutcome::Enqueued
299                } else {
300                    QueuePushOutcome::DroppedStale { dropped }
301                })
302            }
303            PermitRebindOutcome::Rejected(rejection) => Ok(QueuePushOutcome::Rejected {
304                item,
305                rejection: QueuePushRejection::BudgetExhausted(rejection),
306            }),
307        }
308    }
309
310    /// Attempts to push data.
311    pub fn try_push_data(&self, item: T, retained_bytes: usize) -> QueuePushOutcome<T> {
312        self.try_push(item, retained_bytes, BudgetClass::Data)
313    }
314
315    /// Attempts to push control.
316    pub fn try_push_control(&self, item: T, retained_bytes: usize) -> QueuePushOutcome<T> {
317        self.try_push(item, retained_bytes, BudgetClass::Control)
318    }
319
320    /// Pushes one item, waiting for count and byte capacity until an absolute
321    /// Tokio deadline when this queue uses [`FullPolicy::WaitUntilDeadline`].
322    ///
323    /// Other full policies retain their existing immediate behavior. Rate
324    /// exhaustion remains an immediate rejection because capacity-release notifications do not represent token
325    /// refill time.
326    ///
327    /// Every rejection contains the original item.
328    pub async fn push_until(
329        &self,
330        item: T,
331        retained_bytes: usize,
332        class: BudgetClass,
333        deadline: Instant,
334    ) -> QueuePushOutcome<T> {
335        if self.inner.budget.limit().full_policy != FullPolicy::WaitUntilDeadline {
336            return self.try_push(item, retained_bytes, class);
337        }
338        if self.is_closed() {
339            return QueuePushOutcome::Rejected {
340                item,
341                rejection: QueuePushRejection::Closed,
342            };
343        }
344        if let Some(rejection) = self.inner.budget.permanent_acquire_rejection(retained_bytes, class) {
345            return QueuePushOutcome::Rejected {
346                item,
347                rejection: if rejection.is_closed() {
348                    QueuePushRejection::Closed
349                } else {
350                    QueuePushRejection::BudgetExhausted(rejection)
351                },
352            };
353        }
354
355        let mut waiter = None;
356        loop {
357            let capacity_notified = self.inner.budget.capacity_notify().notified();
358            tokio::pin!(capacity_notified);
359            capacity_notified.as_mut().enable();
360            let state_notified = self.inner.notify.notified();
361            tokio::pin!(state_notified);
362            state_notified.as_mut().enable();
363
364            if self.is_closed() {
365                return QueuePushOutcome::Rejected {
366                    item,
367                    rejection: QueuePushRejection::Closed,
368                };
369            }
370            if Instant::now() >= deadline {
371                self.inner.metrics.record_deadline_exceeded();
372                return QueuePushOutcome::Rejected {
373                    item,
374                    rejection: QueuePushRejection::DeadlineExceeded,
375                };
376            }
377
378            {
379                // Publish the retained item before a dynamic close can wake a
380                // receiver and allow it to conclude that the queue has drained.
381                let admission = match self.inner.budget.admit() {
382                    Ok(admission) => admission,
383                    Err(_) => {
384                        return QueuePushOutcome::Rejected {
385                            item,
386                            rejection: QueuePushRejection::Closed,
387                        }
388                    }
389                };
390                match admission.try_acquire_waiting(retained_bytes, class) {
391                    Ok(permit) => {
392                        return self.enqueue(item, permit).unwrap_or(QueuePushOutcome::Enqueued);
393                    }
394                    Err(error) if error.dimension() == Some(BudgetDimension::Rate) => {
395                        self.inner.budget.record_budget_rejection(&error);
396                        return QueuePushOutcome::Rejected {
397                            item,
398                            rejection: QueuePushRejection::BudgetExhausted(error),
399                        };
400                    }
401                    Err(_) => {}
402                }
403            }
404
405            if waiter.is_none() {
406                waiter = Some(self.inner.metrics.begin_wait());
407            }
408            let deadline_sleep = tokio::time::sleep_until(deadline);
409            tokio::pin!(deadline_sleep);
410            tokio::select! {
411                biased;
412                () = &mut deadline_sleep => {
413                    self.inner.metrics.record_deadline_exceeded();
414                    return QueuePushOutcome::Rejected { item, rejection: QueuePushRejection::DeadlineExceeded };
415                }
416                () = &mut state_notified => {}
417                () = &mut capacity_notified => {}
418            }
419        }
420    }
421
422    /// Removes one item and releases its reservation before returning the payload.
423    ///
424    /// Use [`Self::try_pop_budgeted`] when processing must retain the charge
425    /// after the item leaves the queue.
426    pub fn try_pop(&self) -> Option<T> {
427        self.try_pop_budgeted().map(BudgetedItem::into_item)
428    }
429
430    /// Removes one item while transferring its reservation to the caller.
431    ///
432    /// The charge remains until the returned [`BudgetedItem`] or its permit is
433    /// dropped, consumed, or rebound. Queue depth alone does not measure the
434    /// number of outstanding reservations.
435    pub fn try_pop_budgeted(&self) -> Option<BudgetedItem<T>> {
436        let mut discarded = Vec::new();
437        self.apply_age_policy(&mut discarded);
438        let mut state = self
439            .inner
440            .state
441            .lock()
442            .unwrap_or_else(std::sync::PoisonError::into_inner);
443        state.items.pop_front()
444    }
445
446    /// Retains queued items that satisfy `keep` and releases the resource
447    /// permits owned by every removed item.
448    ///
449    /// The relative order of retained items is preserved.
450    pub fn retain(&self, mut keep: impl FnMut(&T) -> bool) -> usize {
451        let mut state = self
452            .inner
453            .state
454            .lock()
455            .unwrap_or_else(std::sync::PoisonError::into_inner);
456        let previous_len = state.items.len();
457        state.items.retain(|item| keep(&item.item));
458        previous_len - state.items.len()
459    }
460
461    /// Asynchronously receives a payload, releasing its reservation on return.
462    ///
463    /// Returns `None` after closed admission and queued work have drained.
464    pub async fn recv(&self) -> Option<T> {
465        self.recv_budgeted().await.map(BudgetedItem::into_item)
466    }
467
468    /// Asynchronously receives an item with its reservation still attached.
469    ///
470    /// Returns `None` after closed admission and queued work have drained.
471    /// Cancellation of this wait does not consume a queued item.
472    pub async fn recv_budgeted(&self) -> Option<BudgetedItem<T>> {
473        loop {
474            let notified = self.inner.notify.notified();
475            tokio::pin!(notified);
476            notified.as_mut().enable();
477            let budget_changed = self.inner.budget.capacity_notify().notified();
478            tokio::pin!(budget_changed);
479            budget_changed.as_mut().enable();
480            if let Some(item) = self.try_pop_budgeted() {
481                return Some(item);
482            }
483            if self.is_closed() {
484                // An accepted producer can publish between the first pop and
485                // closure observation. Closure has now joined admission, so
486                // this final pop cannot miss a later accepted enqueue.
487                return self.try_pop_budgeted();
488            }
489            tokio::select! {
490                _ = &mut notified => {},
491                _ = &mut budget_changed => {},
492            }
493        }
494    }
495
496    /// Executes close.
497    pub fn close(&self) {
498        let mut state = self
499            .inner
500            .state
501            .lock()
502            .unwrap_or_else(std::sync::PoisonError::into_inner);
503        state.closed = true;
504        drop(state);
505        self.inner.notify.notify_waiters();
506    }
507
508    #[must_use]
509    /// Returns whether closed.
510    pub fn is_closed(&self) -> bool {
511        self.inner.budget.is_closed()
512            || self
513                .inner
514                .state
515                .lock()
516                .unwrap_or_else(std::sync::PoisonError::into_inner)
517                .closed
518    }
519
520    #[must_use]
521    /// Returns the len.
522    pub fn len(&self) -> usize {
523        self.inner
524            .state
525            .lock()
526            .unwrap_or_else(std::sync::PoisonError::into_inner)
527            .items
528            .len()
529    }
530
531    #[must_use]
532    /// Returns whether empty.
533    pub fn is_empty(&self) -> bool {
534        self.len() == 0
535    }
536
537    #[must_use]
538    /// Returns the snapshot.
539    pub fn snapshot(&self) -> QueueSnapshot {
540        let budget_closed = self.inner.budget.is_closed();
541        let now = self.inner.budget.monotonic_now();
542        let state = self
543            .inner
544            .state
545            .lock()
546            .unwrap_or_else(std::sync::PoisonError::into_inner);
547        let budget = self.inner.budget.snapshot();
548        QueueSnapshot {
549            path: budget.path,
550            depth: state.items.len(),
551            reserved_count: budget.current_count,
552            retained_bytes: budget.current_bytes,
553            oldest_age: state.items.front().map(|entry| now.saturating_sub(entry.enqueued_at)),
554            throttled_count: budget.throttled_count,
555            rejected_count: budget.rejected_count,
556            dropped_count: budget.dropped_count,
557            coalesced_count: budget.coalesced_count,
558            closed_slow_consumer_count: budget.closed_slow_consumer_count,
559            waiters: self.inner.metrics.waiters.load(Ordering::Acquire),
560            wait_count: self.inner.metrics.wait_count.load(Ordering::Relaxed),
561            deadline_exceeded_count: self.inner.metrics.deadline_exceeded_count.load(Ordering::Relaxed),
562            closed: state.closed || budget_closed,
563        }
564    }
565
566    fn enqueue(&self, item: T, permit: ResourcePermit) -> Option<QueuePushOutcome<T>> {
567        let mut state = self
568            .inner
569            .state
570            .lock()
571            .unwrap_or_else(std::sync::PoisonError::into_inner);
572        if state.closed {
573            return Some(QueuePushOutcome::Rejected {
574                item,
575                rejection: QueuePushRejection::Closed,
576            });
577        }
578        state.items.push_back(BudgetedItem {
579            item,
580            enqueued_at: self.inner.budget.monotonic_now(),
581            permit,
582        });
583        drop(state);
584        self.inner.notify.notify_one();
585        None
586    }
587
588    fn handle_full(
589        &self,
590        admission: &BudgetAdmission<'_>,
591        item: T,
592        retained_bytes: usize,
593        class: BudgetClass,
594        rejection: BudgetRejection,
595        discarded: &mut Vec<T>,
596    ) -> QueuePushOutcome<T> {
597        match self.inner.budget.limit().full_policy {
598            FullPolicy::Reject | FullPolicy::WaitUntilDeadline | FullPolicy::DropStale => QueuePushOutcome::Rejected {
599                item,
600                rejection: QueuePushRejection::BudgetExhausted(rejection),
601            },
602            FullPolicy::CoalesceLatest => {
603                if rejection.dimension() == Some(BudgetDimension::Rate)
604                    || rejection.exhausted_path() != self.inner.budget.path()
605                {
606                    return QueuePushOutcome::Rejected {
607                        item,
608                        rejection: QueuePushRejection::BudgetExhausted(rejection),
609                    };
610                }
611                if !self.item_can_fit(retained_bytes, class) {
612                    return QueuePushOutcome::Rejected {
613                        item,
614                        rejection: QueuePushRejection::BudgetExhausted(rejection),
615                    };
616                }
617                let replaced = {
618                    let mut state = self
619                        .inner
620                        .state
621                        .lock()
622                        .unwrap_or_else(std::sync::PoisonError::into_inner);
623                    if state.closed {
624                        return QueuePushOutcome::Rejected {
625                            item,
626                            rejection: QueuePushRejection::Closed,
627                        };
628                    }
629                    let replaced = state.items.len();
630                    discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
631                    replaced
632                };
633                self.inner.budget.record_coalesced(replaced);
634                let permit = match admission.try_acquire(retained_bytes, class) {
635                    Ok(permit) => permit,
636                    Err(retry_error) => {
637                        return QueuePushOutcome::Rejected {
638                            item,
639                            rejection: QueuePushRejection::BudgetExhausted(retry_error),
640                        };
641                    }
642                };
643                self.enqueue(item, permit)
644                    .unwrap_or(QueuePushOutcome::Coalesced { replaced })
645            }
646            FullPolicy::CloseSlowConsumer => {
647                let dropped = {
648                    let mut state = self
649                        .inner
650                        .state
651                        .lock()
652                        .unwrap_or_else(std::sync::PoisonError::into_inner);
653                    state.closed = true;
654                    let dropped = state.items.len();
655                    discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
656                    dropped
657                };
658                self.inner.budget.record_dropped(dropped);
659                self.inner.budget.record_slow_consumer_closed();
660                self.inner.notify.notify_waiters();
661                QueuePushOutcome::Rejected {
662                    item,
663                    rejection: QueuePushRejection::SlowConsumerClosed,
664                }
665            }
666        }
667    }
668
669    fn item_can_fit(&self, retained_bytes: usize, class: BudgetClass) -> bool {
670        let limit = self.inner.budget.limit();
671        let (count_capacity, byte_capacity) = match class {
672            BudgetClass::Data => (
673                limit.capacity.count.saturating_sub(limit.control_reserve.count),
674                limit.capacity.bytes.saturating_sub(limit.control_reserve.bytes),
675            ),
676            BudgetClass::Control => (limit.capacity.count, limit.capacity.bytes),
677        };
678        count_capacity > 0 && retained_bytes <= byte_capacity
679    }
680
681    fn apply_age_policy(&self, discarded: &mut Vec<T>) -> usize {
682        let Some(max_age) = self.inner.budget.limit().max_age else {
683            return 0;
684        };
685        let policy = self.inner.budget.limit().full_policy;
686        if !matches!(policy, FullPolicy::DropStale | FullPolicy::CloseSlowConsumer) {
687            return 0;
688        }
689        let now = self.inner.budget.monotonic_now();
690        let mut state = self
691            .inner
692            .state
693            .lock()
694            .unwrap_or_else(std::sync::PoisonError::into_inner);
695        if policy == FullPolicy::CloseSlowConsumer
696            && state
697                .items
698                .front()
699                .is_some_and(|entry| now.saturating_sub(entry.enqueued_at) >= max_age)
700        {
701            state.closed = true;
702            let dropped = state.items.len();
703            discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
704            drop(state);
705            self.inner.budget.record_dropped(dropped);
706            self.inner.budget.record_slow_consumer_closed();
707            self.inner.notify.notify_waiters();
708            return dropped;
709        }
710        let mut dropped = 0;
711        while state
712            .items
713            .front()
714            .is_some_and(|entry| now.saturating_sub(entry.enqueued_at) >= max_age)
715        {
716            if let Some(item) = state.items.pop_front() {
717                discarded.push(item.into_item());
718            }
719            dropped += 1;
720        }
721        drop(state);
722        self.inner.budget.record_dropped(dropped);
723        dropped
724    }
725}
726
727struct QueueInner<T> {
728    budget: ResourceBudget,
729    state: Mutex<QueueState<T>>,
730    coalesce_push: Mutex<()>,
731    notify: Notify,
732    metrics: QueueMetrics,
733}
734
735struct QueueState<T> {
736    items: VecDeque<BudgetedItem<T>>,
737    closed: bool,
738}
739
740#[derive(Default)]
741struct QueueMetrics {
742    waiters: AtomicUsize,
743    wait_count: AtomicU64,
744    deadline_exceeded_count: AtomicU64,
745}
746
747impl QueueMetrics {
748    fn begin_wait(&self) -> WaiterGuard<'_> {
749        self.wait_count.fetch_add(1, Ordering::Relaxed);
750        self.waiters.fetch_add(1, Ordering::AcqRel);
751        WaiterGuard { metrics: self }
752    }
753
754    fn record_deadline_exceeded(&self) {
755        self.deadline_exceeded_count.fetch_add(1, Ordering::Relaxed);
756    }
757}
758
759struct WaiterGuard<'a> {
760    metrics: &'a QueueMetrics,
761}
762
763impl Drop for WaiterGuard<'_> {
764    fn drop(&mut self) {
765        let previous = self.metrics.waiters.fetch_sub(1, Ordering::AcqRel);
766        debug_assert!(previous > 0);
767    }
768}