Skip to main content

cf_mach/nq_core/body/
counting_body.rs

1// Copyright (c) 2023-2024 Cloudflare, Inc.
2// Licensed under the BSD-3-Clause license found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
3
4use 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/// [`BodyEvent`]s are generated by a [`CountingBody`] and describe the number
13/// of total bytes seen or that the body has finished.
14#[derive(Debug)]
15pub enum BodyEvent {
16    /// The number of bytes sent by the wrapped body at the given [`Timestamp`].
17    ByteCount {
18        /// When the event was generated.
19        at: Timestamp,
20        /// The total number of bytes seen.
21        total: usize,
22    },
23    /// The [`CountingBody`] has finished sending the body it wraps.
24    Finished {
25        /// When the body finished.
26        at: Timestamp,
27    },
28    /// The transfer terminated early with an error and will produce no further
29    /// bytes.
30    ///
31    /// Emitted either by the [`CountingBody`] itself when the wrapped body
32    /// yields an error, or by the client when the request fails or the server
33    /// rejects it (e.g. an HTTP 413 on an upload). Consumers must treat this as
34    /// terminal: the transfer did *not* complete.
35    Failed {
36        /// When the failure was observed.
37        at: Timestamp,
38        /// Human-readable cause, e.g. `"unexpected status 413 Payload Too Large"`.
39        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    /// Create a [`CountingBody`] by wrapping the given body. Updates are sent
59    /// every `update_every` duration and timestamps are taken with the given
60    /// [`Arc<dyn Time>`].
61    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    /// A handle for reporting a failure the body itself cannot observe, such as
91    /// an upload rejected by the server with a non-success status.
92    ///
93    /// The returned sender must be dropped as soon as it is no longer needed.
94    /// [`CountingBody`] is otherwise the sole owner of the sender, and
95    /// consumers rely on the channel closing when the body is dropped to detect
96    /// a transfer that died without reporting anything.
97    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        // stop the body if there's no event sender.
119        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                // We've waited long enough, send an update.
135                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                    // We can drop the error here since this is an
146                    // increasing counter. The next send will hopefully
147                    // capture it.
148                    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            // Stream finished, send the last count
167            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                // Report the failure so consumers retire this transfer instead
195                // of leaving it looking permanently in-flight. Only emitted
196                // once, and never after a `Finished`.
197                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    // Regression test for the upload completion logic: a fully-consumed body
230    // must emit exactly one `Finished` event.
231    #[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    /// A body that yields one data frame and then errors, standing in for a
252    /// transfer killed mid-flight (reset stream, dropped connection).
253    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    // A body that dies mid-transfer must report `Failed`, otherwise consumers
276    // cannot distinguish it from one that is still running.
277    #[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        // Drive the body until it errors.
284        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}