use flume::Sender;
use std::{
num::NonZeroUsize,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use thiserror::Error;
pub(crate) struct CountedSender<T> {
item_tx: Sender<T>,
quota: Quota,
}
impl<T> CountedSender<T> {
pub(crate) fn new(item_tx: Sender<T>, quota: Quota) -> Self {
Self { item_tx, quota }
}
pub(crate) fn try_send_counted(&self, item: T) -> Result<(), CountedSendError> {
match self.quota.reserve() {
Ok(reservation) => {
self.item_tx.send(item).map_err(|_| Disconnected)?;
reservation.commit();
Ok(())
}
Err(_) if self.item_tx.is_disconnected() => Err(Disconnected.into()),
Err(full) => Err(full.into()),
}
}
pub(crate) fn try_send_uncounted(&self, item: T) -> Result<(), Disconnected> {
self.item_tx.send(item).map_err(|_| Disconnected)
}
}
impl<T> Clone for CountedSender<T> {
fn clone(&self) -> Self {
Self {
item_tx: self.item_tx.clone(),
quota: self.quota.clone(),
}
}
}
#[derive(Debug, Error)]
pub(crate) enum CountedSendError {
#[error(transparent)]
Full(#[from] Full),
#[error(transparent)]
Disconnected(#[from] Disconnected),
}
#[derive(Debug, Error)]
#[error("channel disconnected")]
pub(crate) struct Disconnected;
#[derive(Clone)]
pub(crate) struct Quota(Repr);
impl Quota {
pub(crate) fn unbounded() -> Self {
Self(Repr::Unbounded)
}
pub(crate) fn bounded(capacity: NonZeroUsize) -> Self {
Self(Repr::Bounded {
capacity,
count: Arc::new(AtomicUsize::new(0)),
})
}
pub(crate) fn reserve(&self) -> Result<Reservation<'_>, Full> {
let Repr::Bounded { capacity, count } = &self.0 else {
return Ok(Reservation(Some(self)));
};
count
.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
(current < capacity.get()).then_some(current + 1)
})
.map(|_| Reservation(Some(self)))
.map_err(|_| Full)
}
pub(crate) fn unreserve(&self) {
if let Repr::Bounded { count, .. } = &self.0 {
let previous = count.fetch_sub(1, Ordering::Relaxed);
debug_assert!(previous > 0, "quota count underflow");
}
}
}
pub(crate) struct Reservation<'a>(Option<&'a Quota>);
impl Reservation<'_> {
pub(crate) fn commit(mut self) {
self.0 = None;
}
}
impl Drop for Reservation<'_> {
fn drop(&mut self) {
if let Some(quota) = self.0 {
quota.unreserve();
}
}
}
#[derive(Debug, Error)]
#[error("no capacity left")]
pub(crate) struct Full;
#[derive(Clone)]
enum Repr {
Unbounded,
Bounded {
capacity: NonZeroUsize,
count: Arc<AtomicUsize>,
},
}
#[cfg(test)]
mod tests {
use crate::quota::{CountedSendError, CountedSender, Quota};
use std::num::NonZeroUsize;
#[test]
fn disconnect_overrides_full() {
let (item_tx, item_rx) = flume::unbounded();
let item_tx = CountedSender::new(item_tx, Quota::bounded(NonZeroUsize::MIN));
assert!(item_tx.try_send_counted(()).is_ok());
drop(item_rx);
assert!(matches!(
item_tx.try_send_counted(()),
Err(CountedSendError::Disconnected(_))
));
}
#[test]
fn unbounded_never_fills() {
let quota = Quota::unbounded();
for _ in 0..1_000 {
assert!(quota.reserve().is_ok());
}
quota.unreserve();
}
#[test]
fn bounded_admits_exactly_capacity() {
let capacity = NonZeroUsize::new(3).expect("3 is not zero");
let quota = Quota::bounded(capacity);
let mut reservations = (0..capacity.get())
.map(|_| quota.reserve().expect("capacity is not exhausted yet"))
.collect::<Vec<_>>();
assert!(quota.reserve().is_err());
reservations.pop();
let _reservation = quota.reserve().expect("the released slot is available");
assert!(quota.reserve().is_err());
}
#[test]
fn clones_share_the_count() {
let quota = Quota::bounded(NonZeroUsize::MIN);
let clone = quota.clone();
let reservation = quota.reserve().expect("the empty quota has capacity");
assert!(clone.reserve().is_err());
reservation.commit();
clone.unreserve();
assert!(quota.reserve().is_ok());
}
#[test]
fn sender_clones_share_the_quota() {
let (item_tx, _item_rx) = flume::unbounded();
let item_tx = CountedSender::new(item_tx, Quota::bounded(NonZeroUsize::MIN));
let clone = item_tx.clone();
assert!(item_tx.try_send_counted(()).is_ok());
assert!(matches!(
clone.try_send_counted(()),
Err(CountedSendError::Full(_))
));
}
#[test]
fn a_failed_send_releases_its_reservation() {
let (item_tx, item_rx) = flume::unbounded();
let quota = Quota::bounded(NonZeroUsize::MIN);
let item_tx = CountedSender::new(item_tx, quota.clone());
drop(item_rx);
assert!(matches!(
item_tx.try_send_counted(()),
Err(CountedSendError::Disconnected(_))
));
assert!(quota.reserve().is_ok());
}
#[test]
fn uncounted_bypasses_a_full_quota() {
let (item_tx, item_rx) = flume::unbounded();
let item_tx = CountedSender::new(item_tx, Quota::bounded(NonZeroUsize::MIN));
assert!(item_tx.try_send_counted(1).is_ok());
assert!(matches!(
item_tx.try_send_counted(2),
Err(CountedSendError::Full(_))
));
assert!(item_tx.try_send_uncounted(3).is_ok());
assert_eq!(item_rx.drain().collect::<Vec<_>>(), vec![1, 3]);
}
}