use std::num::NonZeroUsize;
use std::task::{Context, Poll};
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::{SendError, TryRecvError};
use tokio::time::Instant;
use super::load::{self, Channel, LoadObserver};
pub enum Sender<T> {
Unbounded(mpsc::UnboundedSender<T>),
Bounded {
tx: mpsc::Sender<T>,
obs: SendObs,
},
}
#[derive(Clone)]
pub struct SendObs {
channel: Channel,
observer: LoadObserver,
}
pub enum Receiver<T> {
Unbounded(mpsc::UnboundedReceiver<T>),
Bounded(mpsc::Receiver<T>),
}
#[cfg(test)]
pub fn channel<T>(capacity: Option<NonZeroUsize>) -> (Sender<T>, Receiver<T>) {
build(
capacity,
SendObs {
channel: Channel::Shared,
observer: LoadObserver::default(),
},
)
}
pub fn channel_observed<T>(
capacity: Option<NonZeroUsize>,
channel: Channel,
observer: LoadObserver,
) -> (Sender<T>, Receiver<T>) {
build(capacity, SendObs { channel, observer })
}
fn build<T>(capacity: Option<NonZeroUsize>, obs: SendObs) -> (Sender<T>, Receiver<T>) {
capacity.map_or_else(
|| {
let (tx, rx) = mpsc::unbounded_channel();
(Sender::Unbounded(tx), Receiver::Unbounded(rx))
},
|capacity| {
let (tx, rx) = mpsc::channel(capacity.get());
(Sender::Bounded { tx, obs }, Receiver::Bounded(rx))
},
)
}
impl<T> Clone for Sender<T> {
fn clone(&self) -> Self {
match self {
Self::Unbounded(tx) => Self::Unbounded(tx.clone()),
Self::Bounded { tx, obs } => Self::Bounded {
tx: tx.clone(),
obs: obs.clone(),
},
}
}
}
impl<T> Sender<T> {
pub async fn send(&self, value: T) -> Result<(), SendError<T>> {
match self {
Self::Unbounded(tx) => tx.send(value),
Self::Bounded { tx, obs } => match tx.try_send(value) {
Ok(()) => Ok(()),
Err(mpsc::error::TrySendError::Closed(value)) => Err(SendError(value)),
Err(mpsc::error::TrySendError::Full(value)) => {
let started = Instant::now();
let _blocked = obs.observer.track_blocked();
let accepted = tx.send(value).await;
if accepted.is_ok() {
load::capacity_wait(obs.channel, started.elapsed());
}
accepted
}
},
}
}
#[cfg(test)]
pub fn try_send(&self, value: T) -> Result<(), mpsc::error::TrySendError<T>> {
match self {
Self::Unbounded(tx) => tx
.send(value)
.map_err(|SendError(value)| mpsc::error::TrySendError::Closed(value)),
Self::Bounded { tx, .. } => tx.try_send(value),
}
}
#[cfg(any(test, feature = "bench-internals"))]
pub const fn from_unbounded(tx: mpsc::UnboundedSender<T>) -> Self {
Self::Unbounded(tx)
}
}
impl<T> Receiver<T> {
pub fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Option<T>> {
match self {
Self::Unbounded(rx) => rx.poll_recv(cx),
Self::Bounded(rx) => rx.poll_recv(cx),
}
}
pub fn try_recv(&mut self) -> Result<T, TryRecvError> {
match self {
Self::Unbounded(rx) => rx.try_recv(),
Self::Bounded(rx) => rx.try_recv(),
}
}
pub fn len(&self) -> usize {
match self {
Self::Unbounded(rx) => rx.len(),
Self::Bounded(rx) => rx.len(),
}
}
pub fn is_closed(&self) -> bool {
match self {
Self::Unbounded(rx) => rx.is_closed(),
Self::Bounded(rx) => rx.is_closed(),
}
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::num::NonZeroUsize;
use super::*;
use crate::noop_waker::noop_context;
fn cap(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).expect("capacity must be non-zero")
}
#[expect(
clippy::panic,
reason = "a suspended send is the failure this helper reports"
)]
fn send_now<T>(sender: &Sender<T>, value: T) -> Result<(), SendError<T>> {
let fut = sender.send(value);
futures::pin_mut!(fut);
match fut.poll(&mut noop_context()) {
Poll::Ready(result) => result,
Poll::Pending => panic!("send should complete without suspending"),
}
}
#[test]
fn unbounded_send_never_suspends() {
let (tx, _rx) = channel::<i32>(None);
for value in 0..10_000 {
send_now(&tx, value).expect("unbounded receiver is open");
}
}
#[test]
fn bounded_send_waits_at_capacity_and_drops_nothing() {
let (tx, mut rx) = channel::<i32>(Some(cap(2)));
send_now(&tx, 1).expect("slot available");
send_now(&tx, 2).expect("slot available");
let third = tx.send(3);
futures::pin_mut!(third);
assert!(
third.as_mut().poll(&mut noop_context()).is_pending(),
"a send past capacity must wait, not complete or drop"
);
assert_eq!(rx.try_recv(), Ok(1));
assert!(
third.as_mut().poll(&mut noop_context()).is_ready(),
"the waiting send completes once a slot frees"
);
assert_eq!(rx.try_recv(), Ok(2));
assert_eq!(rx.try_recv(), Ok(3));
assert_eq!(rx.try_recv(), Err(TryRecvError::Empty));
}
#[test]
fn independent_bounded_channels_do_not_share_capacity() {
let (tx_a, _rx_a) = channel::<i32>(Some(cap(1)));
let (tx_b, mut rx_b) = channel::<i32>(Some(cap(1)));
send_now(&tx_a, 1).expect("A slot available");
let a_next = tx_a.send(2);
futures::pin_mut!(a_next);
assert!(a_next.as_mut().poll(&mut noop_context()).is_pending());
send_now(&tx_b, 10).expect("B has its own slot");
assert_eq!(rx_b.try_recv(), Ok(10));
}
#[tokio::test(start_paused = true)]
async fn bounded_send_emits_capacity_wait_on_acceptance() {
use tokio::task::yield_now;
use tokio::time::{Duration, advance};
use crate::test_support::TraceRecorder;
let recorder = TraceRecorder::new().with_target("tears::runtime::load");
let _guard = recorder.set_default();
let (tx, mut rx) =
channel_observed::<i32>(Some(cap(1)), Channel::Shared, LoadObserver::default());
tx.send(1).await.expect("first send fits the empty slot");
let sender = tx.clone();
let blocked = tokio::spawn(async move { sender.send(2).await });
yield_now().await;
advance(Duration::from_millis(5)).await;
assert_eq!(rx.try_recv(), Ok(1), "freeing a slot unblocks the send");
blocked
.await
.expect("blocked send task joins")
.expect("the send is accepted once a slot frees");
assert_eq!(
recorder.str_values("channel"),
vec!["shared".to_owned()],
"exactly one capacity-wait event, naming the shared channel"
);
let waits = recorder.u64_values("wait_us");
assert_eq!(waits.len(), 1, "the immediate first send fired no event");
assert!(
waits[0] >= 5_000,
"wait_us reflects the ~5ms blocked interval, got {}",
waits[0]
);
let blocked = recorder.u64_values("blocked");
assert!(
blocked.contains(&1),
"blocked rose while the send waited: {blocked:?}"
);
assert_eq!(
blocked.last(),
Some(&0),
"blocked fell once the send was accepted: {blocked:?}"
);
}
#[tokio::test]
async fn unbounded_observed_channel_emits_no_load_events() {
use crate::test_support::TraceRecorder;
let recorder = TraceRecorder::new().with_target("tears::runtime::load");
let _guard = recorder.set_default();
let (tx, _rx) = channel_observed::<i32>(None, Channel::Shared, LoadObserver::default());
for value in 0..1_000 {
tx.send(value)
.await
.expect("the unbounded receiver is open");
}
assert_eq!(
recorder.event_count(),
0,
"unbounded mode fires no capacity-wait and no blocked-gauge events"
);
assert!(recorder.str_values("channel").is_empty());
assert!(recorder.u64_values("blocked").is_empty());
}
#[tokio::test(flavor = "current_thread")]
async fn blocked_gauge_falls_when_a_blocked_send_is_aborted() {
use tokio::task::yield_now;
use crate::test_support::TraceRecorder;
let recorder = TraceRecorder::new().with_target("tears::runtime::load");
let _guard = recorder.set_default();
let (tx, _rx) =
channel_observed::<i32>(Some(cap(1)), Channel::Keyed, LoadObserver::default());
tx.send(1).await.expect("first send fills the only slot");
let sender = tx.clone();
let blocked = tokio::spawn(async move { sender.send(2).await });
yield_now().await;
blocked.abort();
let _ = blocked.await;
yield_now().await;
let blocked_values = recorder.u64_values("blocked");
assert!(
blocked_values.contains(&1),
"blocked rose while the send waited: {blocked_values:?}"
);
assert_eq!(
blocked_values.last(),
Some(&0),
"aborting the blocked send lowered blocked: {blocked_values:?}"
);
assert!(
recorder.str_values("channel").is_empty(),
"no capacity-wait event fires: the send was never accepted"
);
}
}