use bytes::Bytes;
use futures_util::Stream;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use tokio::sync::mpsc;
use tracing::{debug, warn};
pub const DEFAULT_SUBSCRIBER_CAPACITY: usize = 8192;
pub struct OutputStream {
rx: mpsc::Receiver<Bytes>,
lagged: Arc<AtomicBool>,
}
impl OutputStream {
pub async fn recv(&mut self) -> Option<Bytes> {
self.rx.recv().await
}
pub fn lagged(&self) -> bool {
self.lagged.load(Ordering::Acquire)
}
fn ended() -> Self {
let (_tx, rx) = mpsc::channel(1);
Self {
rx,
lagged: Arc::new(AtomicBool::new(false)),
}
}
}
impl Stream for OutputStream {
type Item = Bytes;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Bytes>> {
self.rx.poll_recv(cx)
}
}
impl std::fmt::Debug for OutputStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OutputStream")
.field("lagged", &self.lagged())
.finish()
}
}
#[derive(Debug)]
struct Subscriber {
tx: mpsc::Sender<Bytes>,
lagged: Arc<AtomicBool>,
}
#[derive(Debug, Default)]
pub(crate) struct OutputFanout {
subscribers: Mutex<Vec<Subscriber>>,
closed: AtomicBool,
}
impl OutputFanout {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn subscribe(&self, capacity: usize) -> OutputStream {
let (tx, rx) = mpsc::channel(capacity.max(1));
let lagged = Arc::new(AtomicBool::new(false));
let mut subscribers = self.lock();
if self.closed.load(Ordering::Acquire) {
return OutputStream::ended();
}
subscribers.push(Subscriber {
tx,
lagged: Arc::clone(&lagged),
});
OutputStream { rx, lagged }
}
pub(crate) fn send(&self, data: Bytes) {
let mut subscribers = self.lock();
subscribers.retain(|sub| match sub.tx.try_send(data.clone()) {
Ok(()) => true,
Err(mpsc::error::TrySendError::Full(_)) => {
warn!(
bytes = data.len(),
"output subscriber is not keeping up; evicting it to protect the session"
);
sub.lagged.store(true, Ordering::Release);
false
}
Err(mpsc::error::TrySendError::Closed(_)) => false,
});
}
pub(crate) fn close(&self) {
if self.closed.swap(true, Ordering::AcqRel) {
return;
}
debug!("closing session output fan-out");
self.lock().clear();
}
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Subscriber>> {
self.subscribers.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt;
#[tokio::test]
async fn every_subscriber_sees_every_chunk() {
let fanout = OutputFanout::new();
let mut a = fanout.subscribe(8);
let mut b = fanout.subscribe(8);
fanout.send(Bytes::from_static(b"one"));
fanout.send(Bytes::from_static(b"two"));
assert_eq!(a.next().await.unwrap(), Bytes::from_static(b"one"));
assert_eq!(a.next().await.unwrap(), Bytes::from_static(b"two"));
assert_eq!(b.next().await.unwrap(), Bytes::from_static(b"one"));
assert_eq!(b.next().await.unwrap(), Bytes::from_static(b"two"));
}
#[tokio::test]
async fn close_ends_open_streams() {
let fanout = OutputFanout::new();
let mut stream = fanout.subscribe(8);
fanout.send(Bytes::from_static(b"before"));
fanout.close();
assert_eq!(stream.next().await.unwrap(), Bytes::from_static(b"before"));
assert!(stream.next().await.is_none());
assert!(!stream.lagged(), "a clean close is not a lag");
}
#[tokio::test]
async fn subscribing_after_close_yields_an_ended_stream() {
let fanout = OutputFanout::new();
fanout.close();
let mut stream = fanout.subscribe(8);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn slow_subscriber_is_evicted_and_reports_lagging() {
let fanout = OutputFanout::new();
let mut slow = fanout.subscribe(2);
let mut healthy = fanout.subscribe(64);
for i in 0..10u8 {
fanout.send(Bytes::from(vec![i]));
}
let mut received = 0;
while slow.next().await.is_some() {
received += 1;
}
assert!(
received <= 2,
"received {received} chunks from a depth-2 queue"
);
assert!(slow.lagged(), "eviction must be observable");
for i in 0..10u8 {
assert_eq!(healthy.next().await.unwrap(), Bytes::from(vec![i]));
}
}
#[tokio::test]
async fn dropped_subscribers_are_reaped() {
let fanout = OutputFanout::new();
let keep = fanout.subscribe(8);
drop(fanout.subscribe(8));
fanout.send(Bytes::from_static(b"x"));
assert_eq!(
fanout.lock().len(),
1,
"the dropped subscriber must be reaped"
);
drop(keep);
}
#[tokio::test]
async fn close_is_idempotent() {
let fanout = OutputFanout::new();
fanout.close();
fanout.close();
let mut stream = fanout.subscribe(4);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn idle_stream_parks_instead_of_spinning() {
let fanout = OutputFanout::new();
let mut stream = fanout.subscribe(8);
let result =
tokio::time::timeout(std::time::Duration::from_millis(50), stream.next()).await;
assert!(result.is_err(), "an idle stream must not resolve");
}
}