use std::{sync::Arc, task::Poll, time::Duration};
use hyper::body::{Body, Bytes};
use tokio::sync::mpsc;
use tracing::{debug, error, trace};
use crate::nq_core::{Time, Timestamp};
#[derive(Debug)]
pub enum BodyEvent {
ByteCount {
at: Timestamp,
total: usize,
},
Finished {
at: Timestamp,
},
Failed {
at: Timestamp,
reason: String,
},
}
pin_project_lite::pin_project! {
#[allow(missing_docs)]
pub struct CountingBody<B> {
#[pin]
inner: B,
time: Arc<dyn Time>,
last_sent: Timestamp,
update_every: Duration,
total: usize,
events_tx: mpsc::UnboundedSender<BodyEvent>,
sent_finished: bool,
}
}
impl<B> CountingBody<B> {
pub fn new(
inner: B,
update_every: Duration,
time: Arc<dyn Time>,
) -> (Self, mpsc::UnboundedReceiver<BodyEvent>) {
let (events_tx, events_rx) = mpsc::unbounded_channel();
let last_sent = time.now();
events_tx
.send(BodyEvent::ByteCount {
at: last_sent,
total: 0,
})
.expect("no data buffered");
(
Self {
inner,
time,
last_sent,
update_every,
total: 0,
events_tx,
sent_finished: false,
},
events_rx,
)
}
pub fn sender(&self) -> mpsc::UnboundedSender<BodyEvent> {
self.events_tx.clone()
}
}
impl<B> Body for CountingBody<B>
where
B: Body<Data = Bytes>,
B::Error: std::fmt::Debug,
{
type Data = B::Data;
type Error = B::Error;
#[inline(always)]
fn poll_frame(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
let mut this = self.project();
if this.events_tx.is_closed() {
debug!("events_tx is closed, stopping");
return Poll::Ready(None);
}
trace!("polling frame");
match this.inner.as_mut().poll_frame(cx) {
Poll::Ready(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
*this.total += data.len();
}
let now = this.time.now();
if now.duration_since(*this.last_sent) >= *this.update_every {
let event = BodyEvent::ByteCount {
at: now,
total: *this.total,
};
*this.last_sent = now;
debug!(?event, "sending event");
let _ = this.events_tx.send(event);
}
if this.inner.is_end_stream() && !*this.sent_finished {
debug!(
total = *this.total,
"body reached end of stream, sending finished event"
);
let _ = this.events_tx.send(BodyEvent::ByteCount {
at: now,
total: *this.total,
});
let _ = this.events_tx.send(BodyEvent::Finished { at: now });
*this.sent_finished = true;
}
Poll::Ready(Some(Ok(frame)))
}
Poll::Ready(None) => {
let now = this.time.now();
let event = BodyEvent::ByteCount {
at: now,
total: *this.total,
};
if !*this.sent_finished {
debug!(
?event,
total = *this.total,
"sending final byte count event"
);
let _ = this.events_tx.send(event);
debug!(at=?now, "sending finished event");
let _ = this.events_tx.send(BodyEvent::Finished { at: now });
*this.sent_finished = true;
} else {
debug!("already sent finish");
}
Poll::Ready(None)
}
Poll::Ready(Some(Err(e))) => {
let now = this.time.now();
error!(error=?e, "body errored");
if !*this.sent_finished {
let _ = this.events_tx.send(BodyEvent::Failed {
at: now,
reason: format!("body error: {e:?}"),
});
*this.sent_finished = true;
}
Poll::Ready(Some(Err(e)))
}
Poll::Pending => {
trace!("body pending");
Poll::Pending
}
}
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> hyper::body::SizeHint {
self.inner.size_hint()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nq_core::{TokioTime, body::UploadBody};
use http_body_util::BodyExt;
#[tokio::test]
async fn upload_body_emits_finished() {
let size = 512 * 1024;
let body = UploadBody::new(size);
let time: Arc<dyn Time> = Arc::new(TokioTime::new());
let (body, mut events) = CountingBody::new(body, Duration::ZERO, time);
body.collect().await.unwrap();
events.close();
let mut got_finished = false;
while let Some(ev) = events.recv().await {
if matches!(ev, BodyEvent::Finished { .. }) {
assert!(!got_finished, "duplicate Finished");
got_finished = true;
}
}
assert!(got_finished, "never received Finished");
}
struct ErroringBody {
sent: bool,
}
impl Body for ErroringBody {
type Data = Bytes;
type Error = &'static str;
fn poll_frame(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
if !self.sent {
self.sent = true;
return Poll::Ready(Some(Ok(hyper::body::Frame::data(Bytes::from_static(
b"hello",
)))));
}
Poll::Ready(Some(Err("stream reset")))
}
}
#[tokio::test]
async fn errored_body_emits_failed() {
let time: Arc<dyn Time> = Arc::new(TokioTime::new());
let (body, mut events) =
CountingBody::new(ErroringBody { sent: false }, Duration::ZERO, time);
let _ = body.collect().await;
events.close();
let mut failed = None;
let mut got_finished = false;
while let Some(ev) = events.recv().await {
match ev {
BodyEvent::Failed { reason, .. } => failed = Some(reason),
BodyEvent::Finished { .. } => got_finished = true,
BodyEvent::ByteCount { .. } => {}
}
}
assert!(failed.is_some(), "never received Failed");
assert!(!got_finished, "a failed body must not also report Finished");
}
}