use std::time::Duration;
use tokio::sync::mpsc::Sender;
pub const DEFAULT_ORPHAN_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SendFailure {
Disconnected,
Orphaned,
}
pub fn orphan_timeout_from_env() -> Option<Duration> {
match std::env::var("FERROX_SSE_ORPHAN_TIMEOUT_MS") {
Err(_) => Some(DEFAULT_ORPHAN_TIMEOUT),
Ok(raw) => match raw.trim().parse::<u64>() {
Ok(0) => None,
Ok(ms) => Some(Duration::from_millis(ms)),
Err(_) => {
tracing::warn!(
"FERROX_SSE_ORPHAN_TIMEOUT_MS='{raw}' is not a non-negative integer; \
using the {DEFAULT_ORPHAN_TIMEOUT:?} default"
);
Some(DEFAULT_ORPHAN_TIMEOUT)
}
},
}
}
pub fn send_or_orphan<T>(
tx: &Sender<T>,
value: T,
timeout: Option<Duration>,
) -> Result<(), SendFailure> {
let Some(timeout) = timeout else {
return tx
.blocking_send(value)
.map_err(|_| SendFailure::Disconnected);
};
let handle = match tokio::runtime::Handle::try_current() {
Ok(handle) => handle,
Err(_) => {
return tx
.blocking_send(value)
.map_err(|_| SendFailure::Disconnected);
}
};
match handle.block_on(tx.send_timeout(value, timeout)) {
Ok(()) => Ok(()),
Err(tokio::sync::mpsc::error::SendTimeoutError::Closed(_)) => {
Err(SendFailure::Disconnected)
}
Err(tokio::sync::mpsc::error::SendTimeoutError::Timeout(_)) => Err(SendFailure::Orphaned),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[tokio::test]
async fn a_receiver_that_never_reads_does_not_park_the_sender_forever() {
let (tx, _rx_held_and_never_polled) = tokio::sync::mpsc::channel::<u32>(1);
tx.send(1).await.expect("the first send fills the channel");
let done = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&done);
let result = tokio::task::spawn_blocking(move || {
let out = send_or_orphan(&tx, 2, Some(Duration::from_millis(50)));
flag.store(true, Ordering::SeqCst);
out
});
let out = tokio::time::timeout(Duration::from_secs(5), result)
.await
.expect("the send must give up rather than park forever")
.expect("the blocking task must not panic");
assert_eq!(out, Err(SendFailure::Orphaned));
assert!(done.load(Ordering::SeqCst));
}
#[tokio::test]
async fn a_dropped_receiver_is_a_disconnect_and_is_reported_at_once() {
let (tx, rx) = tokio::sync::mpsc::channel::<u32>(1);
drop(rx);
let started = std::time::Instant::now();
let out = tokio::task::spawn_blocking(move || {
send_or_orphan(&tx, 1, Some(Duration::from_secs(30)))
})
.await
.unwrap();
assert_eq!(out, Err(SendFailure::Disconnected));
assert!(
started.elapsed() < Duration::from_secs(5),
"a closed channel must not wait out the orphan deadline"
);
}
#[tokio::test]
async fn a_draining_receiver_gets_every_event_in_order() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<u32>(2);
let sender = tokio::task::spawn_blocking(move || {
for i in 0..64 {
send_or_orphan(&tx, i, Some(Duration::from_secs(30)))?;
}
Ok::<(), SendFailure>(())
});
let mut got = Vec::new();
while let Some(v) = rx.recv().await {
got.push(v);
}
sender
.await
.unwrap()
.expect("a drained channel never fails");
assert_eq!(got, (0..64).collect::<Vec<_>>());
}
#[tokio::test]
async fn no_deadline_still_delivers() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<u32>(1);
let sender = tokio::task::spawn_blocking(move || send_or_orphan(&tx, 7, None));
assert_eq!(rx.recv().await, Some(7));
sender.await.unwrap().expect("a live receiver accepts");
}
#[test]
fn the_default_deadline_applies_when_the_env_var_is_absent() {
assert_eq!(DEFAULT_ORPHAN_TIMEOUT, Duration::from_secs(30));
}
}