1use 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#[derive(Debug)]
38pub enum QueuePushOutcome<T> {
39 Enqueued,
41 Coalesced {
43 replaced: usize,
45 },
46 DroppedStale {
48 dropped: usize,
50 },
51 Rejected {
53 item: T,
55 rejection: QueuePushRejection,
57 },
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum QueuePushRejection {
63 BudgetExhausted(BudgetRejection),
65 DeadlineExceeded,
67 Closed,
69 SlowConsumerClosed,
71}
72
73#[derive(Debug)]
83pub struct ForeignPermit<T> {
84 pub item: T,
86 pub permit: Box<ResourcePermit>,
88}
89
90pub struct BudgetedItem<T> {
92 item: T,
93 enqueued_at: Duration,
94 permit: ResourcePermit,
95}
96
97impl<T> BudgetedItem<T> {
98 #[must_use]
99 pub fn into_item(self) -> T {
101 self.item
102 }
103
104 #[must_use]
105 pub fn into_parts(self) -> (T, ResourcePermit, Duration) {
107 (self.item, self.permit, self.enqueued_at)
108 }
109
110 #[must_use]
111 pub const fn enqueued_at(&self) -> Duration {
113 self.enqueued_at
114 }
115
116 #[must_use]
117 pub fn retained_bytes(&self) -> usize {
119 self.permit.bytes()
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct QueueSnapshot {
126 pub path: Arc<str>,
128 pub depth: usize,
130 pub reserved_count: usize,
132 pub retained_bytes: usize,
134 pub oldest_age: Option<Duration>,
136 pub throttled_count: u64,
138 pub rejected_count: u64,
140 pub dropped_count: u64,
142 pub coalesced_count: u64,
144 pub closed_slow_consumer_count: u64,
146 pub waiters: usize,
148 pub wait_count: u64,
150 pub deadline_exceeded_count: u64,
152 pub closed: bool,
154}
155
156pub 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 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 #[must_use]
198 pub fn is_same_queue(&self, other: &Self) -> bool {
199 Arc::ptr_eq(&self.inner, &other.inner)
200 }
201
202 pub fn try_push(&self, item: T, retained_bytes: usize, class: BudgetClass) -> QueuePushOutcome<T> {
204 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 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 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 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 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 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 pub fn try_pop(&self) -> Option<T> {
427 self.try_pop_budgeted().map(BudgetedItem::into_item)
428 }
429
430 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 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 pub async fn recv(&self) -> Option<T> {
465 self.recv_budgeted().await.map(BudgetedItem::into_item)
466 }
467
468 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 return self.try_pop_budgeted();
488 }
489 tokio::select! {
490 _ = &mut notified => {},
491 _ = &mut budget_changed => {},
492 }
493 }
494 }
495
496 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 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 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 pub fn is_empty(&self) -> bool {
534 self.len() == 0
535 }
536
537 #[must_use]
538 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}