use std::convert::Infallible;
use std::time::Duration;
use axum::body::Bytes;
use tokio::sync::broadcast::error::RecvError;
use tracing::debug;
use super::MachineHistory;
use super::events::{lagged_frame, sse_frame};
pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(20);
const HEARTBEAT: &[u8] = b": heartbeat\n\n";
pub async fn event_stream(
history: &MachineHistory,
) -> impl futures_util::Stream<Item = Result<Bytes, Infallible>> + Send + 'static {
let (snapshot, rx) = history.subscribe().await;
let head = futures_util::stream::iter(std::iter::once(Ok(sse_frame("history", &snapshot))));
let heartbeat = tokio::time::interval_at(
tokio::time::Instant::now() + HEARTBEAT_INTERVAL,
HEARTBEAT_INTERVAL,
);
let tail = futures_util::stream::unfold(Some((rx, heartbeat)), |state| async move {
let (mut rx, mut heartbeat) = state?;
tokio::select! {
biased;
received = rx.recv() => match received {
Ok(event) => Some((Ok(event.frame()), Some((rx, heartbeat)))),
Err(RecvError::Lagged(dropped)) => {
debug!(dropped, "machine_history: subscriber lagged the event buffer");
Some((Ok(lagged_frame(dropped)), Some((rx, heartbeat))))
}
Err(RecvError::Closed) => None,
},
_ = heartbeat.tick() => Some((
Ok(Bytes::from_static(HEARTBEAT)),
Some((rx, heartbeat)),
)),
}
});
futures_util::StreamExt::chain(head, tail)
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt as _;
use trusty_common::host_metrics::HostSampler;
fn parse(frame: &Bytes) -> (String, serde_json::Value) {
let text = String::from_utf8(frame.to_vec()).expect("utf8 frame");
let (name, rest) = text
.strip_prefix("event: ")
.and_then(|r| r.split_once('\n'))
.unwrap_or_else(|| panic!("frame has no event line: {text:?}"));
let data = rest
.strip_prefix("data: ")
.and_then(|d| d.strip_suffix("\n\n"))
.unwrap_or_else(|| panic!("frame has no data line: {text:?}"));
(
name.to_string(),
serde_json::from_str(data).expect("frame data is json"),
)
}
#[tokio::test]
async fn a_late_subscriber_gets_the_window_then_the_next_sample() {
let history = MachineHistory::new();
let mut sampler = HostSampler::new();
for _ in 0..3 {
history.record_sample(sampler.sample()).await;
}
let mut stream = Box::pin(event_stream(&history).await);
let fourth = sampler.sample();
let fourth_cores = fourth.cpu.logical_cores;
history.record_sample(fourth).await;
let first = stream
.next()
.await
.expect("history frame")
.expect("infallible");
let (name, data) = parse(&first);
assert_eq!(name, "history");
assert_eq!(
data["samples"].as_array().expect("samples array").len(),
3,
"the connecting subscriber gets the three samples it missed"
);
assert_eq!(data["sample_capacity"], 120);
assert_eq!(data["sample_interval_secs"], 5);
let second = stream
.next()
.await
.expect("live frame")
.expect("infallible");
let (name, data) = parse(&second);
assert_eq!(name, "sample", "the fourth sample arrives live");
assert_eq!(data["cpu"]["logical_cores"], fourth_cores);
}
#[tokio::test]
async fn a_lagging_subscriber_is_told_what_it_missed() {
let history = MachineHistory::with_limits(120, 16, 2, Duration::from_secs(60));
let mut stream = Box::pin(event_stream(&history).await);
let mut sampler = HostSampler::new();
for _ in 0..5 {
history.record_sample(sampler.sample()).await;
}
let first = stream
.next()
.await
.expect("history frame")
.expect("infallible");
assert_eq!(parse(&first).0, "history");
let second = stream
.next()
.await
.expect("lagged frame")
.expect("infallible");
let (name, data) = parse(&second);
assert_eq!(name, "lagged", "the gap is reported, not hidden");
assert!(
data["dropped"].as_u64().expect("dropped count") >= 1,
"the dropped count is reported: {data}"
);
}
}