use std::convert::Infallible;
use std::time::Duration;
use axum::response::sse::Event;
use futures_util::Stream;
use serde::Serialize;
use tokio::sync::mpsc::{Receiver, Sender};
pub(crate) const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
pub(crate) fn keepalive_event<T: Serialize>(payload: &T) -> Event {
let data = serde_json::to_string(payload).unwrap_or_else(|e| {
tracing::error!("failed to serialize a keepalive frame: {e}");
"{}".to_string()
});
Event::default().data(data)
}
pub(crate) fn with_keepalive(
events: Receiver<Result<Event, Infallible>>,
keepalive: Event,
interval: Duration,
) -> impl Stream<Item = Result<Event, Infallible>> {
futures_util::stream::unfold(
(events, keepalive),
move |(mut events, keepalive)| async move {
match tokio::time::timeout(interval, events.recv()).await {
Err(_elapsed) => Some((Ok(keepalive.clone()), (events, keepalive))),
Ok(Some(event)) => Some((event, (events, keepalive))),
Ok(None) => None,
}
},
)
}
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));
}
#[tokio::test]
async fn a_stream_that_has_not_started_still_sends_keepalives() {
use futures_util::StreamExt;
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Event, Infallible>>(4);
let keepalive = keepalive_event(&serde_json::json!({"ping": true}));
let mut stream = Box::pin(with_keepalive(rx, keepalive, Duration::from_millis(20)));
for _ in 0..2 {
assert!(
stream.next().await.is_some(),
"a quiet stream keeps talking"
);
}
tx.send(Ok(Event::default().data("real"))).await.unwrap();
assert!(stream.next().await.is_some());
drop(tx);
assert!(
stream.next().await.is_none(),
"a closed channel ends the stream rather than keeping it alive forever"
);
}
#[test]
fn a_keepalive_carries_data_and_not_a_comment() {
let event = keepalive_event(&serde_json::json!({"object": "chat.completion.chunk"}));
let wire = format!("{:?}", event);
assert!(
wire.contains("chat.completion.chunk"),
"the payload must be in the frame, not just implied: {wire}"
);
}
}