Skip to main content

weighted_mpsc/
lib.rs

1//! A bounded [tokio] mpsc channel that bounds the queue by the total *weight* of
2//! the messages in it, rather than by the number of messages.
3//!
4//! Each type sent through the channel reports a weight via [`Weigh`] - usually its
5//! size in bytes, but any additive measure works (rows, estimated cost, etc.). The
6//! channel has a fixed weight budget; a send waits until the messages already in
7//! the channel leave enough room for the new one, then goes through. This lets you
8//! cap the memory (or any other weighed resource) a producer/consumer pipeline
9//! holds at once, even when messages vary a lot in size.
10//!
11//! A [`tokio::sync::Semaphore`] tracks the budget: one permit per weight unit. A
12//! message takes permits equal to its weight while it is in the channel and while
13//! the receiver still holds the [`Lease`] that [`recv`] returns. The permits go
14//! back to the budget when the `Lease` is dropped, which frees room for more sends.
15//! Holding the `Lease` while you use the value keeps the bound accurate; dropping
16//! it (or calling [`Lease::into_inner`]) frees the room immediately.
17//!
18//! [`recv`]: WeightedReceiver::recv
19//!
20//! ```no_run
21//! use weighted_mpsc::channel;
22//!
23//! # async fn example() {
24//! // Cap the in-flight bytes at 1 MiB; the count buffer (16) is a backstop.
25//! let (tx, mut rx) = channel::<Vec<u8>>(16, 1024 * 1024);
26//!
27//! tx.send(vec![0u8; 256 * 1024]).await.unwrap();
28//!
29//! // `msg` derefs to the Vec<u8>; the budget is held until `msg` is dropped.
30//! let msg = rx.recv().await.unwrap();
31//! assert_eq!(msg.len(), 256 * 1024);
32//! # }
33//! ```
34//!
35//! [tokio]: https://docs.rs/tokio
36
37use std::fmt;
38use std::ops::{Deref, DerefMut};
39#[cfg(feature = "stream")]
40use std::pin::Pin;
41use std::sync::Arc;
42#[cfg(feature = "stream")]
43use std::task::{Context, Poll};
44
45use tokio::sync::mpsc;
46use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError};
47
48/// The weight budget is capped at `u32::MAX` (about 4 GiB) because a single
49/// `acquire_many` takes a `u32`; [`Builder::build`] panics above that. Individual
50/// messages may weigh more - they are handled as oversized (see [`Oversized`]).
51const MAX_PERMITS: usize = u32::MAX as usize;
52
53/// A value that can report its own weight in the unit the channel's budget uses.
54///
55/// Weight is almost always a byte count, but it can be any additive measure: the
56/// budget is just the total weight allowed in the channel at once. Report the cost
57/// that dominates the resource you want to bound (usually heap bytes); it does not
58/// need to be exact.
59pub trait Weigh {
60    /// The weight of this value. Bytes is the common case.
61    fn weight(&self) -> usize;
62}
63
64/// Convenience [`Weigh`] impls for common owned byte/text containers (weight =
65/// byte length), behind the default `weigh-std` feature. Turn off default features
66/// to drop them, e.g. to weigh one of these types differently via a newtype.
67#[cfg(feature = "weigh-std")]
68mod weigh_std {
69    use super::Weigh;
70
71    impl Weigh for Vec<u8> {
72        fn weight(&self) -> usize {
73            self.len()
74        }
75    }
76
77    impl Weigh for String {
78        fn weight(&self) -> usize {
79            self.len()
80        }
81    }
82
83    impl Weigh for Box<[u8]> {
84        fn weight(&self) -> usize {
85            self.len()
86        }
87    }
88}
89
90/// [`Weigh`] impls for the [`bytes`](https://docs.rs/bytes) types (weight = byte
91/// length), behind the `weigh-bytes` feature.
92#[cfg(feature = "weigh-bytes")]
93mod weigh_bytes {
94    use super::Weigh;
95
96    impl Weigh for bytes::Bytes {
97        fn weight(&self) -> usize {
98            self.len()
99        }
100    }
101
102    impl Weigh for bytes::BytesMut {
103        fn weight(&self) -> usize {
104            self.len()
105        }
106    }
107}
108
109/// What to do with a message that weighs more than the whole budget.
110///
111/// Such a message cannot fit even in an empty channel, so waiting for room would
112/// wait forever. This selects the alternative. The choice is independent of
113/// [`Builder::on_oversized`], which observes the event either way.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum Oversized {
116    /// Send the message anyway. It reserves the entire budget while it is in the
117    /// channel, so nothing else can be in flight until it is received and its
118    /// [`Lease`] dropped. The message is delivered whole: no bytes are dropped or
119    /// truncated. This is the default.
120    ///
121    /// Note the message is fully in memory while in flight, so an oversized message
122    /// makes peak memory briefly exceed the budget by about its own size. Size the
123    /// budget at or above your largest message if you need it to be a hard ceiling.
124    #[default]
125    Allow,
126    /// Do not send it: [`WeightedSender::send`] returns [`SendError::TooLarge`] and
127    /// the budget is left untouched.
128    Reject,
129    /// Discard it: `send` returns `Ok(())` without sending. Pair it with
130    /// [`Builder::on_oversized`] to count or log the discards.
131    Drop,
132}
133
134/// The error returned by [`WeightedSender::send`].
135pub enum SendError<T> {
136    /// The receiver (and every clone of it) was dropped, so the message could not
137    /// be delivered. Does not consume the message.
138    Closed(T),
139    /// The message weighs more than the whole budget and the channel's [`Oversized`]
140    /// policy is [`Oversized::Reject`]. Does not consume the message.
141    TooLarge(T),
142}
143
144impl<T> SendError<T> {
145    /// Recover the message that could not be sent.
146    pub fn into_inner(self) -> T {
147        match self {
148            SendError::Closed(v) | SendError::TooLarge(v) => v,
149        }
150    }
151}
152
153impl<T> fmt::Debug for SendError<T> {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            SendError::Closed(_) => f.write_str("SendError::Closed(..)"),
157            SendError::TooLarge(_) => f.write_str("SendError::TooLarge(..)"),
158        }
159    }
160}
161
162impl<T> fmt::Display for SendError<T> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            SendError::Closed(_) => f.write_str("channel closed: the receiver was dropped"),
166            SendError::TooLarge(_) => f.write_str("message weighs more than the channel budget"),
167        }
168    }
169}
170
171impl<T> std::error::Error for SendError<T> {}
172
173/// The error returned by [`WeightedSender::try_send`].
174pub enum TrySendError<T> {
175    /// There is no room right now: admitting the message would exceed the weight
176    /// budget, or the count buffer is full. A later `try_send` may succeed once the
177    /// receiver drains room. Does not consume the message.
178    Full(T),
179    /// The receiver (and every clone of it) was dropped, so the message could not
180    /// be delivered. Does not consume the message.
181    Closed(T),
182    /// The message weighs more than the whole budget and the channel's [`Oversized`]
183    /// policy is [`Oversized::Reject`]. Does not consume the message.
184    TooLarge(T),
185}
186
187impl<T> TrySendError<T> {
188    /// Recover the message that could not be sent.
189    pub fn into_inner(self) -> T {
190        match self {
191            TrySendError::Full(v) | TrySendError::Closed(v) | TrySendError::TooLarge(v) => v,
192        }
193    }
194}
195
196impl<T> fmt::Debug for TrySendError<T> {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        match self {
199            TrySendError::Full(_) => f.write_str("TrySendError::Full(..)"),
200            TrySendError::Closed(_) => f.write_str("TrySendError::Closed(..)"),
201            TrySendError::TooLarge(_) => f.write_str("TrySendError::TooLarge(..)"),
202        }
203    }
204}
205
206impl<T> fmt::Display for TrySendError<T> {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        match self {
209            TrySendError::Full(_) => {
210                f.write_str("channel full: no room in the weight budget or count buffer")
211            }
212            TrySendError::Closed(_) => f.write_str("channel closed: the receiver was dropped"),
213            TrySendError::TooLarge(_) => f.write_str("message weighs more than the channel budget"),
214        }
215    }
216}
217
218impl<T> std::error::Error for TrySendError<T> {}
219
220/// Observer invoked when a message weighs more than the whole budget: called with
221/// `(message_weight, max_weight)`.
222type OversizedHook = Arc<dyn Fn(usize, usize) + Send + Sync>;
223
224/// Builder for a weighted channel. Use [`channel`] for the common case.
225#[derive(Clone)]
226pub struct Builder {
227    buffer: usize,
228    max_weight: usize,
229    min_weight: usize,
230    oversized: Oversized,
231    on_oversized: Option<OversizedHook>,
232}
233
234impl Builder {
235    /// Create a builder with the two required bounds:
236    ///
237    /// - `buffer`: the message-count buffer of the underlying tokio channel. It is
238    ///   the backstop for zero- or near-zero-weight messages, which the weight
239    ///   budget alone would not bound. Must be `> 0`.
240    /// - `max_weight`: the total weight allowed in the channel at once. Must be in
241    ///   `1..=u32::MAX` (about 4 GiB).
242    ///
243    /// Defaults: [`Oversized::Allow`], `min_weight` of 1, and no observer.
244    pub fn new(buffer: usize, max_weight: usize) -> Self {
245        Self {
246            buffer,
247            max_weight,
248            min_weight: 1,
249            oversized: Oversized::default(),
250            on_oversized: None,
251        }
252    }
253
254    /// Minimum weight counted for any message. A message lighter than this still
255    /// takes this much budget, so a flood of tiny messages cannot fill the channel
256    /// without the budget noticing. Defaults to 1; set it to 0 to let zero-weight
257    /// messages through without taking any budget (then only `buffer` bounds them).
258    pub fn min_weight(mut self, min_weight: usize) -> Self {
259        self.min_weight = min_weight;
260        self
261    }
262
263    /// How to handle a message that weighs more than the whole budget. See
264    /// [`Oversized`].
265    pub fn oversized(mut self, policy: Oversized) -> Self {
266        self.oversized = policy;
267        self
268    }
269
270    /// Register an observer called as `(message_weight, max_weight)` whenever a
271    /// message weighs more than the budget, whatever the [`Oversized`] choice.
272    pub fn on_oversized<F>(mut self, hook: F) -> Self
273    where
274        F: Fn(usize, usize) + Send + Sync + 'static,
275    {
276        self.on_oversized = Some(Arc::new(hook));
277        self
278    }
279
280    /// Build the sender/receiver pair.
281    ///
282    /// # Panics
283    ///
284    /// Panics if `buffer` is 0, or if `max_weight` is 0 or greater than `u32::MAX`.
285    pub fn build<T>(self) -> (WeightedSender<T>, WeightedReceiver<T>) {
286        assert!(self.buffer > 0, "buffer must be > 0");
287        assert!(self.max_weight > 0, "max_weight must be > 0");
288        // A single `acquire_many` takes a `u32`, so the budget cannot exceed that.
289        // Fail loudly here rather than silently shrink what the caller asked for.
290        assert!(
291            self.max_weight <= MAX_PERMITS,
292            "max_weight must be <= u32::MAX (about 4 GiB)"
293        );
294
295        let budget = Arc::new(Semaphore::new(self.max_weight));
296        let (tx, rx) = mpsc::channel(self.buffer);
297
298        let sender = WeightedSender {
299            tx,
300            budget: Arc::clone(&budget),
301            max_weight: self.max_weight,
302            min_weight: self.min_weight,
303            oversized: self.oversized,
304            on_oversized: self.on_oversized,
305        };
306        let receiver = WeightedReceiver { rx, budget };
307        (sender, receiver)
308    }
309}
310
311impl fmt::Debug for Builder {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        f.debug_struct("Builder")
314            .field("buffer", &self.buffer)
315            .field("max_weight", &self.max_weight)
316            .field("min_weight", &self.min_weight)
317            .field("oversized", &self.oversized)
318            .finish_non_exhaustive()
319    }
320}
321
322/// Convenience constructor for a weighted channel with the default policy
323/// ([`Oversized::Allow`], `min_weight` 1). For anything else, use [`Builder`].
324///
325/// See [`Builder::new`] for the meaning of `buffer` and `max_weight`.
326///
327/// # Panics
328///
329/// Panics if `buffer` or `max_weight` is 0.
330pub fn channel<T>(buffer: usize, max_weight: usize) -> (WeightedSender<T>, WeightedReceiver<T>) {
331    Builder::new(buffer, max_weight).build()
332}
333
334/// The sending half of a weighted channel. Cloneable and shareable across tasks.
335pub struct WeightedSender<T> {
336    tx: mpsc::Sender<Lease<T>>,
337    budget: Arc<Semaphore>,
338    max_weight: usize,
339    min_weight: usize,
340    oversized: Oversized,
341    on_oversized: Option<OversizedHook>,
342}
343
344// Manual: `T` need not be `Clone` for the sender to be.
345impl<T> Clone for WeightedSender<T> {
346    fn clone(&self) -> Self {
347        Self {
348            tx: self.tx.clone(),
349            budget: Arc::clone(&self.budget),
350            max_weight: self.max_weight,
351            min_weight: self.min_weight,
352            oversized: self.oversized,
353            on_oversized: self.on_oversized.clone(),
354        }
355    }
356}
357
358impl<T: Weigh> WeightedSender<T> {
359    /// Send a message, waiting until there is room in the budget for its weight.
360    ///
361    /// Waits (asynchronously) while the messages already in the channel plus this
362    /// one would exceed the budget, then delivers it. A message that weighs more
363    /// than the whole budget is handled per the channel's [`Oversized`] policy.
364    ///
365    /// # Errors
366    ///
367    /// - [`SendError::Closed`] if the receiver has been dropped.
368    /// - [`SendError::TooLarge`] if the message weighs more than the budget and the
369    ///   policy is [`Oversized::Reject`].
370    pub async fn send(&self, value: T) -> Result<(), SendError<T>> {
371        let weight = value.weight().max(self.min_weight);
372
373        let reserve = if weight > self.max_weight {
374            if let Some(hook) = &self.on_oversized {
375                hook(weight, self.max_weight);
376            }
377            match self.oversized {
378                Oversized::Reject => return Err(SendError::TooLarge(value)),
379                Oversized::Drop => return Ok(()),
380                // Cannot reserve more than exists, so reserve all of it. The message
381                // is still delivered whole.
382                Oversized::Allow => self.max_weight,
383            }
384        } else {
385            weight
386        };
387
388        // `reserve <= max_weight <= MAX_PERMITS` (checked in `build`), so the cast
389        // is lossless.
390        let permit = match Arc::clone(&self.budget)
391            .acquire_many_owned(reserve as u32)
392            .await
393        {
394            Ok(permit) => permit,
395            // The budget is only ever closed by the receiver being dropped (see
396            // `WeightedReceiver::drop`), which also unblocks a send waiting here.
397            Err(_) => return Err(SendError::Closed(value)),
398        };
399
400        self.tx
401            .send(Lease {
402                value,
403                weight: reserve,
404                _permit: permit,
405            })
406            .await
407            .map_err(|e| SendError::Closed(e.0.value))
408    }
409
410    /// Try to send a message without waiting.
411    ///
412    /// Like [`send`](Self::send), but never waits: if admitting the message would
413    /// exceed the budget, or the count buffer is full, it returns
414    /// [`TrySendError::Full`] right away instead of waiting for room. A message that
415    /// weighs more than the whole budget is handled per the channel's [`Oversized`]
416    /// policy.
417    ///
418    /// # Errors
419    ///
420    /// - [`TrySendError::Full`] if there is no room right now (weight budget or count
421    ///   buffer).
422    /// - [`TrySendError::Closed`] if the receiver has been dropped.
423    /// - [`TrySendError::TooLarge`] if the message weighs more than the budget and
424    ///   the policy is [`Oversized::Reject`].
425    pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>> {
426        let weight = value.weight().max(self.min_weight);
427
428        let reserve = if weight > self.max_weight {
429            if let Some(hook) = &self.on_oversized {
430                hook(weight, self.max_weight);
431            }
432            match self.oversized {
433                Oversized::Reject => return Err(TrySendError::TooLarge(value)),
434                Oversized::Drop => return Ok(()),
435                Oversized::Allow => self.max_weight,
436            }
437        } else {
438            weight
439        };
440
441        // `reserve <= max_weight <= MAX_PERMITS` (checked in `build`), so the cast
442        // is lossless.
443        let permit = match Arc::clone(&self.budget).try_acquire_many_owned(reserve as u32) {
444            Ok(permit) => permit,
445            Err(TryAcquireError::NoPermits) => return Err(TrySendError::Full(value)),
446            // The budget is only ever closed by the receiver being dropped (see
447            // `WeightedReceiver::drop`).
448            Err(TryAcquireError::Closed) => return Err(TrySendError::Closed(value)),
449        };
450
451        // On `Full`/`Closed` the returned `Lease` is dropped as we recover the value,
452        // which returns the permit to the budget; only the value goes to the caller.
453        self.tx
454            .try_send(Lease {
455                value,
456                weight: reserve,
457                _permit: permit,
458            })
459            .map_err(|e| match e {
460                mpsc::error::TrySendError::Full(lease) => TrySendError::Full(lease.into_inner()),
461                mpsc::error::TrySendError::Closed(lease) => {
462                    TrySendError::Closed(lease.into_inner())
463                }
464            })
465    }
466
467    /// The channel's total weight budget (the value passed to [`Builder::new`]).
468    pub fn max_weight(&self) -> usize {
469        self.max_weight
470    }
471
472    /// Budget not currently reserved by messages in the channel, in weight units.
473    pub fn available_weight(&self) -> usize {
474        self.budget.available_permits()
475    }
476}
477
478impl<T> fmt::Debug for WeightedSender<T> {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        f.debug_struct("WeightedSender")
481            .field("max_weight", &self.max_weight)
482            .field("min_weight", &self.min_weight)
483            .field("oversized", &self.oversized)
484            .field("available_weight", &self.budget.available_permits())
485            .finish_non_exhaustive()
486    }
487}
488
489/// The receiving half of a weighted channel.
490///
491/// Dropping the receiver closes the channel: any producer waiting for room in
492/// [`WeightedSender::send`] wakes with [`SendError::Closed`].
493pub struct WeightedReceiver<T> {
494    rx: mpsc::Receiver<Lease<T>>,
495    budget: Arc<Semaphore>,
496}
497
498impl<T> WeightedReceiver<T> {
499    /// Receive the next message, or `None` once every sender is dropped and the
500    /// channel is drained.
501    ///
502    /// The returned [`Lease`] holds the message's budget until it is dropped, so
503    /// hold it while the message is in use, or call [`Lease::into_inner`] to take
504    /// the value and free the budget now.
505    pub async fn recv(&mut self) -> Option<Lease<T>> {
506        self.rx.recv().await
507    }
508
509    /// Close the channel without dropping the receiver: senders stop, but messages
510    /// already in the channel can still be drained with [`recv`](Self::recv).
511    pub fn close(&mut self) {
512        self.rx.close();
513        self.budget.close();
514    }
515}
516
517impl<T> Drop for WeightedReceiver<T> {
518    fn drop(&mut self) {
519        // Wake any sender waiting on `acquire_many_owned`; without this a producer
520        // waiting for room when the last receiver goes away would hang forever,
521        // since dropping the receiver does not by itself return permits.
522        self.budget.close();
523    }
524}
525
526impl<T> fmt::Debug for WeightedReceiver<T> {
527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528        f.debug_struct("WeightedReceiver")
529            .field("available_weight", &self.budget.available_permits())
530            .finish_non_exhaustive()
531    }
532}
533
534/// The receiver is a [`Stream`] of [`Lease`]s (behind the `stream` feature). Each
535/// item holds its message's budget until dropped, exactly like
536/// [`recv`](WeightedReceiver::recv); the stream ends once every sender is dropped
537/// and the channel is drained.
538///
539/// Drive it with the combinators from a stream crate, e.g. `futures`:
540///
541/// ```ignore
542/// use futures::StreamExt;
543/// while let Some(lease) = rx.next().await {
544///     // `lease` derefs to the message; its budget is freed when it drops.
545/// }
546/// ```
547///
548/// [`Stream`]: futures_core::Stream
549#[cfg(feature = "stream")]
550impl<T> futures_core::Stream for WeightedReceiver<T> {
551    type Item = Lease<T>;
552
553    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
554        self.rx.poll_recv(cx)
555    }
556}
557
558/// A received message together with the budget it holds.
559///
560/// Derefs to the message, so use it as you would the value itself. The budget the
561/// message took is returned to the channel when the `Lease` is dropped - so hold it
562/// while you are still using the value to keep the bound accurate, and drop it (or
563/// call [`into_inner`](Self::into_inner)) to free the room for more sends.
564pub struct Lease<T> {
565    value: T,
566    weight: usize,
567    // Returned to the budget on drop; that release frees room for the next send.
568    // The leading underscore documents that it is held only for its Drop effect.
569    _permit: OwnedSemaphorePermit,
570}
571
572impl<T> Lease<T> {
573    /// Take the message out, releasing its budget.
574    pub fn into_inner(self) -> T {
575        self.value
576    }
577
578    /// Weight reserved for this message (after `min_weight` and the oversized
579    /// policy).
580    pub fn weight(&self) -> usize {
581        self.weight
582    }
583}
584
585impl<T> Deref for Lease<T> {
586    type Target = T;
587    fn deref(&self) -> &T {
588        &self.value
589    }
590}
591
592impl<T> DerefMut for Lease<T> {
593    fn deref_mut(&mut self) -> &mut T {
594        &mut self.value
595    }
596}
597
598impl<T: fmt::Debug> fmt::Debug for Lease<T> {
599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600        f.debug_struct("Lease")
601            .field("value", &self.value)
602            .field("weight", &self.weight)
603            .finish()
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use std::sync::atomic::{AtomicUsize, Ordering};
610    use std::time::Duration;
611
612    use tokio::time::timeout;
613
614    use super::*;
615
616    #[derive(Debug)]
617    struct Msg(Vec<u8>);
618
619    impl Weigh for Msg {
620        fn weight(&self) -> usize {
621            self.0.len()
622        }
623    }
624
625    fn msg(bytes: usize) -> Msg {
626        Msg(vec![0u8; bytes])
627    }
628
629    #[tokio::test]
630    async fn delivers_all_messages() {
631        let (tx, mut rx) = channel::<Msg>(8, 1024 * 1024);
632        // Produce concurrently: with a count buffer of 8 and 10 messages, a
633        // produce-all-then-consume loop would block the 9th send on the buffer.
634        let producer = tokio::spawn(async move {
635            for _ in 0..10 {
636                tx.send(msg(1000)).await.unwrap();
637            }
638        });
639
640        let mut got = 0;
641        while let Some(d) = rx.recv().await {
642            assert_eq!(d.0.len(), 1000);
643            got += 1;
644        }
645        producer.await.unwrap();
646        assert_eq!(got, 10);
647    }
648
649    #[tokio::test]
650    async fn bounds_by_weight_not_count() {
651        // Budget of 1000 bytes, generous count buffer. A second 600-byte message
652        // must wait even though the count buffer is nowhere near full.
653        let (tx, mut rx) = channel::<Msg>(64, 1000);
654        tx.send(msg(600)).await.unwrap();
655        assert_eq!(tx.available_weight(), 400);
656
657        let tx2 = tx.clone();
658        let blocked = tokio::spawn(async move { tx2.send(msg(600)).await });
659
660        // Give the spawned send a chance to run: 600 + 600 > 1000 and nothing has
661        // been consumed, so it must still be waiting for room.
662        tokio::time::sleep(Duration::from_millis(50)).await;
663        assert!(!blocked.is_finished(), "send should be waiting for room");
664
665        // Free the first message; now the second fits.
666        let first = rx.recv().await.unwrap();
667        assert_eq!(first.weight(), 600);
668        drop(first);
669
670        timeout(Duration::from_millis(200), blocked)
671            .await
672            .expect("send should unblock once room frees")
673            .unwrap()
674            .unwrap();
675    }
676
677    #[tokio::test]
678    async fn holding_lease_keeps_budget_reserved() {
679        let (tx, mut rx) = channel::<Msg>(64, 1000);
680        tx.send(msg(400)).await.unwrap();
681        let held = rx.recv().await.unwrap();
682        // The budget stays reserved while the Lease is alive, not just while the
683        // message sits in the channel.
684        assert_eq!(tx.available_weight(), 600);
685        drop(held);
686        assert_eq!(tx.available_weight(), 1000);
687    }
688
689    #[tokio::test]
690    async fn into_inner_frees_budget() {
691        let (tx, mut rx) = channel::<Msg>(64, 1000);
692        tx.send(msg(400)).await.unwrap();
693        let value = rx.recv().await.unwrap().into_inner();
694        // Taking the value out drops the Lease, so the budget is freed immediately.
695        assert_eq!(tx.available_weight(), 1000);
696        assert_eq!(value.0.len(), 400);
697    }
698
699    #[tokio::test]
700    async fn oversized_allow_delivers_whole_message() {
701        let (tx, mut rx) = channel::<Msg>(64, 1000);
702        // 5000 > 1000: delivered whole, but reserves the entire budget.
703        tx.send(msg(5000)).await.unwrap();
704        assert_eq!(tx.available_weight(), 0);
705        let d = rx.recv().await.unwrap();
706        assert_eq!(d.weight(), 1000);
707        assert_eq!(d.0.len(), 5000, "no bytes dropped or truncated");
708    }
709
710    #[tokio::test]
711    async fn oversized_reject_returns_the_message() {
712        let (tx, mut rx) = Builder::new(64, 1000)
713            .oversized(Oversized::Reject)
714            .build::<Msg>();
715        let err = tx.send(msg(5000)).await.unwrap_err();
716        match err {
717            SendError::TooLarge(m) => assert_eq!(m.0.len(), 5000),
718            other => panic!("expected TooLarge, got {other:?}"),
719        }
720        assert_eq!(tx.available_weight(), 1000, "budget untouched on reject");
721        // A normal message still flows.
722        tx.send(msg(10)).await.unwrap();
723        assert!(rx.recv().await.is_some());
724    }
725
726    #[tokio::test]
727    async fn oversized_drop_silently_discards() {
728        let seen = Arc::new(AtomicUsize::new(0));
729        let seen2 = Arc::clone(&seen);
730        let (tx, mut rx) = Builder::new(64, 1000)
731            .oversized(Oversized::Drop)
732            .on_oversized(move |w, max| {
733                assert_eq!((w, max), (5000, 1000));
734                seen2.fetch_add(1, Ordering::SeqCst);
735            })
736            .build::<Msg>();
737
738        tx.send(msg(5000)).await.unwrap(); // discarded, Ok(())
739        assert_eq!(seen.load(Ordering::SeqCst), 1);
740        assert_eq!(tx.available_weight(), 1000);
741
742        tx.send(msg(10)).await.unwrap();
743        let d = rx.recv().await.unwrap();
744        assert_eq!(d.0.len(), 10);
745    }
746
747    #[tokio::test]
748    async fn min_weight_floors_cheap_messages() {
749        // Zero-weight messages would otherwise never take any budget.
750        let (tx, _rx) = Builder::new(64, 10).min_weight(2).build::<Msg>();
751        tx.send(msg(0)).await.unwrap();
752        assert_eq!(tx.available_weight(), 8);
753    }
754
755    #[test]
756    #[should_panic(expected = "max_weight must be <= u32::MAX")]
757    fn rejects_budget_over_u32() {
758        let _ = Builder::new(8, MAX_PERMITS + 1).build::<Msg>();
759    }
760
761    #[cfg(feature = "weigh-std")]
762    #[tokio::test]
763    async fn weighs_std_types() {
764        // Vec<u8> and String are covered by the integration test; check Box<[u8]>.
765        let (tx, _rx) = channel::<Box<[u8]>>(8, 1000);
766        tx.send(vec![0u8; 300].into_boxed_slice()).await.unwrap();
767        assert_eq!(tx.available_weight(), 700);
768    }
769
770    #[cfg(feature = "weigh-bytes")]
771    #[tokio::test]
772    async fn weighs_bytes() {
773        let (tx, mut rx) = channel::<bytes::Bytes>(8, 1000);
774        tx.send(bytes::Bytes::from(vec![0u8; 400])).await.unwrap();
775        assert_eq!(tx.available_weight(), 600);
776        let d = rx.recv().await.unwrap();
777        assert_eq!(d.len(), 400);
778    }
779
780    #[tokio::test]
781    async fn dropping_receiver_unblocks_waiting_sender() {
782        let (tx, rx) = channel::<Msg>(64, 1000);
783        tx.send(msg(1000)).await.unwrap(); // budget now full
784
785        let tx2 = tx.clone();
786        let blocked = tokio::spawn(async move { tx2.send(msg(1000)).await });
787
788        // Drop the receiver while a send is waiting for room.
789        drop(rx);
790
791        let res = timeout(Duration::from_millis(200), blocked)
792            .await
793            .expect("send should wake when the receiver drops")
794            .unwrap();
795        assert!(matches!(res, Err(SendError::Closed(_))));
796    }
797
798    #[tokio::test]
799    async fn send_after_receiver_dropped_is_closed() {
800        let (tx, rx) = channel::<Msg>(8, 1000);
801        drop(rx);
802        let err = tx.send(msg(1)).await.unwrap_err();
803        assert!(matches!(err, SendError::Closed(_)));
804        assert_eq!(err.into_inner().0.len(), 1);
805    }
806
807    #[tokio::test]
808    async fn try_send_delivers_when_room() {
809        let (tx, mut rx) = channel::<Msg>(8, 1000);
810        tx.try_send(msg(400)).unwrap();
811        assert_eq!(tx.available_weight(), 600);
812        let d = rx.recv().await.unwrap();
813        assert_eq!(d.weight(), 400);
814    }
815
816    #[tokio::test]
817    async fn try_send_full_when_budget_exhausted() {
818        let (tx, _rx) = channel::<Msg>(64, 1000);
819        tx.try_send(msg(1000)).unwrap(); // budget now full
820        let err = tx.try_send(msg(1)).unwrap_err();
821        assert!(matches!(err, TrySendError::Full(_)));
822        assert_eq!(err.into_inner().0.len(), 1);
823        // The rejected send left the budget untouched.
824        assert_eq!(tx.available_weight(), 0);
825    }
826
827    #[tokio::test]
828    async fn try_send_full_when_count_buffer_full() {
829        // Count buffer of 1 with a generous budget: the second message is refused by
830        // the buffer, not the budget, and its permit must return to the budget.
831        let (tx, _rx) = channel::<Msg>(1, 1000);
832        tx.try_send(msg(10)).unwrap();
833        let err = tx.try_send(msg(10)).unwrap_err();
834        assert!(matches!(err, TrySendError::Full(_)));
835        assert_eq!(tx.available_weight(), 990, "permit returned on buffer-full");
836    }
837
838    #[tokio::test]
839    async fn try_send_closed_after_receiver_dropped() {
840        let (tx, rx) = channel::<Msg>(8, 1000);
841        drop(rx);
842        let err = tx.try_send(msg(1)).unwrap_err();
843        assert!(matches!(err, TrySendError::Closed(_)));
844        assert_eq!(err.into_inner().0.len(), 1);
845    }
846
847    #[tokio::test]
848    async fn try_send_oversized_reject_returns_the_message() {
849        let (tx, _rx) = Builder::new(64, 1000)
850            .oversized(Oversized::Reject)
851            .build::<Msg>();
852        let err = tx.try_send(msg(5000)).unwrap_err();
853        assert!(matches!(err, TrySendError::TooLarge(_)));
854        assert_eq!(tx.available_weight(), 1000, "budget untouched on reject");
855    }
856
857    #[tokio::test]
858    async fn try_send_oversized_drop_silently_discards() {
859        let seen = Arc::new(AtomicUsize::new(0));
860        let seen2 = Arc::clone(&seen);
861        let (tx, _rx) = Builder::new(64, 1000)
862            .oversized(Oversized::Drop)
863            .on_oversized(move |_, _| {
864                seen2.fetch_add(1, Ordering::SeqCst);
865            })
866            .build::<Msg>();
867        tx.try_send(msg(5000)).unwrap(); // discarded, Ok(())
868        assert_eq!(seen.load(Ordering::SeqCst), 1);
869        assert_eq!(tx.available_weight(), 1000);
870    }
871
872    #[tokio::test]
873    async fn try_send_oversized_allow_reserves_whole_budget() {
874        let (tx, mut rx) = channel::<Msg>(64, 1000);
875        tx.try_send(msg(5000)).unwrap();
876        assert_eq!(tx.available_weight(), 0);
877        let d = rx.recv().await.unwrap();
878        assert_eq!(d.weight(), 1000);
879        assert_eq!(d.0.len(), 5000, "no bytes dropped or truncated");
880    }
881
882    #[cfg(feature = "stream")]
883    #[tokio::test]
884    async fn stream_yields_lease_and_frees_budget_on_drop() {
885        use std::future::poll_fn;
886
887        use futures_core::Stream;
888
889        let (tx, mut rx) = channel::<Msg>(8, 1000);
890        tx.send(msg(400)).await.unwrap();
891
892        // Consume the receiver through its Stream impl rather than `recv`.
893        let lease = poll_fn(|cx| Pin::new(&mut rx).poll_next(cx)).await.unwrap();
894        assert_eq!(lease.weight(), 400);
895        // Like `recv`, the yielded Lease holds the budget until it is dropped.
896        assert_eq!(tx.available_weight(), 600);
897        drop(lease);
898        assert_eq!(tx.available_weight(), 1000);
899    }
900
901    #[cfg(feature = "stream")]
902    #[tokio::test]
903    async fn stream_ends_when_all_senders_dropped() {
904        use std::future::poll_fn;
905
906        use futures_core::Stream;
907
908        let (tx, mut rx) = channel::<Msg>(8, 1000);
909        tx.send(msg(10)).await.unwrap();
910        drop(tx);
911
912        assert!(
913            poll_fn(|cx| Pin::new(&mut rx).poll_next(cx))
914                .await
915                .is_some()
916        );
917        assert!(
918            poll_fn(|cx| Pin::new(&mut rx).poll_next(cx))
919                .await
920                .is_none(),
921            "stream ends after the last sender drops and the channel drains"
922        );
923    }
924}