cf_mach/nq_core/body/
counting_body.rs1use std::{sync::Arc, task::Poll, time::Duration};
5
6use hyper::body::{Body, Bytes};
7use tokio::sync::mpsc;
8use tracing::{debug, error, trace};
9
10use crate::nq_core::{Time, Timestamp};
11
12#[derive(Debug)]
15pub enum BodyEvent {
16 ByteCount {
18 at: Timestamp,
20 total: usize,
22 },
23 Finished {
25 at: Timestamp,
27 },
28 Failed {
36 at: Timestamp,
38 reason: String,
40 },
41}
42
43pin_project_lite::pin_project! {
44 #[allow(missing_docs)]
45 pub struct CountingBody<B> {
46 #[pin]
47 inner: B,
48 time: Arc<dyn Time>,
49 last_sent: Timestamp,
50 update_every: Duration,
51 total: usize,
52 events_tx: mpsc::UnboundedSender<BodyEvent>,
53 sent_finished: bool,
54 }
55}
56
57impl<B> CountingBody<B> {
58 pub fn new(
62 inner: B,
63 update_every: Duration,
64 time: Arc<dyn Time>,
65 ) -> (Self, mpsc::UnboundedReceiver<BodyEvent>) {
66 let (events_tx, events_rx) = mpsc::unbounded_channel();
67 let last_sent = time.now();
68
69 events_tx
70 .send(BodyEvent::ByteCount {
71 at: last_sent,
72 total: 0,
73 })
74 .expect("no data buffered");
75
76 (
77 Self {
78 inner,
79 time,
80 last_sent,
81 update_every,
82 total: 0,
83 events_tx,
84 sent_finished: false,
85 },
86 events_rx,
87 )
88 }
89
90 pub fn sender(&self) -> mpsc::UnboundedSender<BodyEvent> {
98 self.events_tx.clone()
99 }
100}
101
102impl<B> Body for CountingBody<B>
103where
104 B: Body<Data = Bytes>,
105 B::Error: std::fmt::Debug,
106{
107 type Data = B::Data;
108
109 type Error = B::Error;
110
111 #[inline(always)]
112 fn poll_frame(
113 self: std::pin::Pin<&mut Self>,
114 cx: &mut std::task::Context<'_>,
115 ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
116 let mut this = self.project();
117
118 if this.events_tx.is_closed() {
120 debug!("events_tx is closed, stopping");
121 return Poll::Ready(None);
122 }
123
124 trace!("polling frame");
125
126 match this.inner.as_mut().poll_frame(cx) {
127 Poll::Ready(Some(Ok(frame))) => {
128 if let Some(data) = frame.data_ref() {
129 *this.total += data.len();
130 }
131
132 let now = this.time.now();
133
134 if now.duration_since(*this.last_sent) >= *this.update_every {
136 let event = BodyEvent::ByteCount {
137 at: now,
138 total: *this.total,
139 };
140
141 *this.last_sent = now;
142
143 debug!(?event, "sending event");
144
145 let _ = this.events_tx.send(event);
149 }
150
151 if this.inner.is_end_stream() && !*this.sent_finished {
152 debug!(
153 total = *this.total,
154 "body reached end of stream, sending finished event"
155 );
156 let _ = this.events_tx.send(BodyEvent::ByteCount {
157 at: now,
158 total: *this.total,
159 });
160 let _ = this.events_tx.send(BodyEvent::Finished { at: now });
161 *this.sent_finished = true;
162 }
163
164 Poll::Ready(Some(Ok(frame)))
165 }
166 Poll::Ready(None) => {
168 let now = this.time.now();
169 let event = BodyEvent::ByteCount {
170 at: now,
171 total: *this.total,
172 };
173
174 if !*this.sent_finished {
175 debug!(
176 ?event,
177 total = *this.total,
178 "sending final byte count event"
179 );
180 let _ = this.events_tx.send(event);
181 debug!(at=?now, "sending finished event");
182 let _ = this.events_tx.send(BodyEvent::Finished { at: now });
183 *this.sent_finished = true;
184 } else {
185 debug!("already sent finish");
186 }
187
188 Poll::Ready(None)
189 }
190 Poll::Ready(Some(Err(e))) => {
191 let now = this.time.now();
192 error!(error=?e, "body errored");
193
194 if !*this.sent_finished {
198 let _ = this.events_tx.send(BodyEvent::Failed {
199 at: now,
200 reason: format!("body error: {e:?}"),
201 });
202 *this.sent_finished = true;
203 }
204
205 Poll::Ready(Some(Err(e)))
206 }
207 Poll::Pending => {
208 trace!("body pending");
209 Poll::Pending
210 }
211 }
212 }
213
214 fn is_end_stream(&self) -> bool {
215 self.inner.is_end_stream()
216 }
217
218 fn size_hint(&self) -> hyper::body::SizeHint {
219 self.inner.size_hint()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::nq_core::{TokioTime, body::UploadBody};
227 use http_body_util::BodyExt;
228
229 #[tokio::test]
232 async fn upload_body_emits_finished() {
233 let size = 512 * 1024;
234 let body = UploadBody::new(size);
235 let time: Arc<dyn Time> = Arc::new(TokioTime::new());
236 let (body, mut events) = CountingBody::new(body, Duration::ZERO, time);
237
238 body.collect().await.unwrap();
239 events.close();
240
241 let mut got_finished = false;
242 while let Some(ev) = events.recv().await {
243 if matches!(ev, BodyEvent::Finished { .. }) {
244 assert!(!got_finished, "duplicate Finished");
245 got_finished = true;
246 }
247 }
248 assert!(got_finished, "never received Finished");
249 }
250
251 struct ErroringBody {
254 sent: bool,
255 }
256
257 impl Body for ErroringBody {
258 type Data = Bytes;
259 type Error = &'static str;
260
261 fn poll_frame(
262 mut self: std::pin::Pin<&mut Self>,
263 _cx: &mut std::task::Context<'_>,
264 ) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
265 if !self.sent {
266 self.sent = true;
267 return Poll::Ready(Some(Ok(hyper::body::Frame::data(Bytes::from_static(
268 b"hello",
269 )))));
270 }
271 Poll::Ready(Some(Err("stream reset")))
272 }
273 }
274
275 #[tokio::test]
278 async fn errored_body_emits_failed() {
279 let time: Arc<dyn Time> = Arc::new(TokioTime::new());
280 let (body, mut events) =
281 CountingBody::new(ErroringBody { sent: false }, Duration::ZERO, time);
282
283 let _ = body.collect().await;
285 events.close();
286
287 let mut failed = None;
288 let mut got_finished = false;
289 while let Some(ev) = events.recv().await {
290 match ev {
291 BodyEvent::Failed { reason, .. } => failed = Some(reason),
292 BodyEvent::Finished { .. } => got_finished = true,
293 BodyEvent::ByteCount { .. } => {}
294 }
295 }
296
297 assert!(failed.is_some(), "never received Failed");
298 assert!(!got_finished, "a failed body must not also report Finished");
299 }
300}