#![cfg(feature = "tcp")]
use std::time::Duration;
use bytes::Bytes;
use qmux::transport::{Reader, Transport, Writer};
use qmux::{Config, Error, Session, Version};
use tokio::sync::{mpsc, watch};
use web_transport_trait::{RecvStream as _, SendStream as _, Session as _};
struct GatedTransport {
tx: mpsc::Sender<Bytes>,
rx: mpsc::Receiver<Bytes>,
gate: Option<watch::Receiver<bool>>,
}
struct GatedWriter {
tx: mpsc::Sender<Bytes>,
gate: Option<watch::Receiver<bool>>,
}
struct GatedReader {
rx: mpsc::Receiver<Bytes>,
}
impl Transport for GatedTransport {
type Writer = GatedWriter;
type Reader = GatedReader;
fn split(self) -> (GatedWriter, GatedReader) {
(
GatedWriter {
tx: self.tx,
gate: self.gate,
},
GatedReader { rx: self.rx },
)
}
}
impl Writer for GatedWriter {
async fn send(&mut self, data: Bytes) -> Result<(), Error> {
if let Some(gate) = &mut self.gate {
gate.wait_for(|&open| open)
.await
.map_err(|_| Error::Closed)?;
}
self.tx.send(data).await.map_err(|_| Error::Closed)
}
async fn close(&mut self) -> Result<(), Error> {
Ok(())
}
}
impl Reader for GatedReader {
async fn recv(&mut self) -> Result<Bytes, Error> {
self.rx.recv().await.ok_or(Error::Closed)
}
}
async fn pair(gate: watch::Receiver<bool>) -> (Session, Session) {
let (c2s_tx, c2s_rx) = mpsc::channel(256);
let (s2c_tx, s2c_rx) = mpsc::channel(256);
let client_transport = GatedTransport {
tx: c2s_tx,
rx: s2c_rx,
gate: Some(gate),
};
let server_transport = GatedTransport {
tx: s2c_tx,
rx: c2s_rx,
gate: None,
};
let config = Config::new(Version::QMux01);
let (client, server) = tokio::join!(
Session::connect(client_transport, config.clone()),
Session::accept(server_transport, config),
);
(client.unwrap(), server.unwrap())
}
#[tokio::test]
async fn reads_are_not_blocked_by_a_stalled_writer() {
let (gate_tx, gate_rx) = watch::channel(true); let (client, server) = pair(gate_rx).await;
gate_tx.send(false).unwrap();
let client_writer = client.clone();
let writer = tokio::spawn(async move {
let mut s = client_writer.open_uni().await.unwrap();
let _ = s.write(&vec![b'C'; 512 * 1024]).await; client_writer });
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!writer.is_finished(),
"client writer should be parked on the closed gate"
);
let mut ss = server.open_uni().await.unwrap();
ss.write(b"hello from server").await.unwrap();
ss.finish().unwrap();
let mut rs = tokio::time::timeout(Duration::from_secs(2), client.accept_uni())
.await
.expect("accept must not be blocked by the stalled writer")
.unwrap();
let got = tokio::time::timeout(Duration::from_secs(2), rs.read_all())
.await
.expect("read must not be blocked by the stalled writer")
.unwrap();
assert_eq!(&got[..], b"hello from server");
gate_tx.send(true).unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), writer).await;
}
#[tokio::test]
async fn datagrams_are_shed_under_backpressure() {
let (gate_tx, gate_rx) = watch::channel(true);
let (client, server) = pair(gate_rx).await;
assert!(
client.max_datagram_size() > 0,
"datagrams must be enabled for this test"
);
gate_tx.send(false).unwrap();
let client_filler = client.clone();
let filler = tokio::spawn(async move {
let mut s = client_filler.open_uni().await.unwrap();
let _ = s.write(&vec![b'F'; 512 * 1024]).await;
client_filler
});
tokio::time::sleep(Duration::from_millis(50)).await;
const N: usize = 200;
for i in 0..N {
client
.send_datagram(Bytes::from(vec![i as u8; 8]))
.expect("send_datagram is best-effort and must not error when backpressured");
}
gate_tx.send(true).unwrap();
let mut received = 0usize;
while let Ok(Ok(_)) =
tokio::time::timeout(Duration::from_millis(200), server.recv_datagram()).await
{
received += 1;
}
assert!(
received <= 96,
"backpressured datagrams must be shed down to ~the 64-deep lane, but {received}/{N} arrived"
);
filler.abort();
}
#[tokio::test]
async fn idle_timeout_deferred_but_bounded_under_backpressure() {
let (c2s_tx, c2s_rx) = mpsc::channel(256);
let (s2c_tx, s2c_rx) = mpsc::channel(256);
let _s2c_keepalive = s2c_tx.clone();
let (gate_tx, gate_rx) = watch::channel(true); let client_transport = GatedTransport {
tx: c2s_tx,
rx: s2c_rx,
gate: Some(gate_rx.clone()),
};
let server_transport = GatedTransport {
tx: s2c_tx,
rx: c2s_rx,
gate: Some(gate_rx),
};
let mut config = Config::new(Version::QMux01);
config.max_idle_timeout = 150;
let (client, server) = tokio::join!(
Session::connect(client_transport, config.clone()),
Session::accept(server_transport, config),
);
let client = client.unwrap();
let _server = server.unwrap();
gate_tx.send(false).unwrap();
let client_writer = client.clone();
let writer = tokio::spawn(async move {
let mut s = client_writer.open_uni().await.unwrap();
let _ = s.write(&vec![b'X'; 256 * 1024]).await;
client_writer
});
let client_closed = client.clone();
let closed = tokio::spawn(async move { client_closed.closed().await });
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
!closed.is_finished(),
"idle-close must be deferred while the writer is backpressured (within grace)"
);
let reason = tokio::time::timeout(Duration::from_millis(600), closed)
.await
.expect("bounded deferral must eventually idle-close a stuck-backpressured peer")
.unwrap();
assert!(
matches!(reason, Error::IdleTimeout),
"expected IdleTimeout once the grace elapses, got {reason:?}"
);
writer.abort();
}