Skip to main content

cf_mach/nq_core/
client.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
4//! Defines two clients, a [`ThroughputClient`] and a normal [`Client`]. The
5//! [`ThroughputClient`] trackes the sending or receiving of body data and sends
6//! byte count updates to a listener. This is useful for determining the
7//! throughput of a flow.
8
9use std::{convert::Infallible, net::ToSocketAddrs, sync::Arc, time::Duration};
10use tokio::sync::RwLock;
11
12use anyhow::{Context, bail};
13use http::{HeaderMap, HeaderValue, Uri};
14use http_body_util::BodyExt;
15use hyper::body::{Body, Bytes, Incoming};
16use tokio::select;
17use tokio::sync::mpsc;
18use tokio_util::sync::CancellationToken;
19use tracing::{Instrument, debug, error, info, trace};
20
21use crate::nq_core::{
22    ConnectionType, EstablishedConnection, Network, OneshotResult, ScopedHeaders, Time, Timestamp,
23    body::{BodyEvent, CountingBody, InflightBody, NqBody, UploadBody, empty},
24    oneshot_result,
25};
26
27/// The default user agent for networkquality requests
28pub const MACH_USER_AGENT: &str = concat!("mach/", env!("CARGO_PKG_VERSION"));
29
30/// Describes the direction of the client. This determines if the client times
31/// the upload or download of a body.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Direction {
34    /// Download the response body.
35    Down,
36    /// Upload the given number of bytes.
37    Up(usize),
38}
39
40/// A [`ThroughputClient`] is a simple client which drives a request/response pair
41/// and returns an [`InflightBody`].
42///
43/// This should be used if you do not care about the request or response, and just
44/// need to load a connection.
45///
46/// The returned [`InflightBody`] can be used to track the progress of an upload
47/// or download and when it finishes.
48pub struct ThroughputClient {
49    connection: Option<Arc<RwLock<EstablishedConnection>>>,
50    new_connection_type: Option<ConnectionType>,
51    headers: Option<HeaderMap>,
52    scoped_headers: Option<ScopedHeaders>,
53    direction: Direction,
54}
55
56impl ThroughputClient {
57    /// Create an download oriented [`ThroughputClient`].
58    pub fn download() -> Self {
59        Self {
60            connection: None,
61            new_connection_type: None,
62            headers: None,
63            scoped_headers: None,
64            direction: Direction::Down,
65        }
66    }
67
68    /// Create an upload oriented [`ThroughputClient`].
69    pub fn upload(size: usize) -> Self {
70        Self {
71            connection: None,
72            new_connection_type: None,
73            headers: None,
74            scoped_headers: None,
75            direction: Direction::Up(size),
76        }
77    }
78
79    /// Send requests on the given [`EstablishedConnection`].
80    pub fn with_connection(mut self, connection: Arc<RwLock<EstablishedConnection>>) -> Self {
81        self.connection = Some(connection);
82        self
83    }
84
85    /// Create a new connection for each request.
86    pub fn new_connection(mut self, conn_type: ConnectionType) -> Self {
87        self.new_connection_type = Some(conn_type);
88        self
89    }
90
91    /// Set the headers for the upload or download request.
92    pub fn headers(mut self, headers: HeaderMap<HeaderValue>) -> Self {
93        self.headers = Some(headers);
94        self
95    }
96
97    /// Set headers that are only attached when the request's host matches the
98    /// scope's allowlist. Headers set via [`Self::headers`] take precedence.
99    pub fn scoped_headers(mut self, scoped_headers: Option<ScopedHeaders>) -> Self {
100        self.scoped_headers = scoped_headers;
101        self
102    }
103
104    /// Execute a download or upload request against the given [`Uri`].
105    // #[tracing::instrument(skip(self, network, time, shutdown))]
106    pub fn send(
107        mut self,
108        uri: Uri,
109        network: Arc<dyn Network>,
110        time: Arc<dyn Time>,
111        shutdown: CancellationToken,
112    ) -> anyhow::Result<OneshotResult<InflightBody>> {
113        debug!("headers: {:?}", self.headers);
114        let mut headers = self.headers.take().unwrap_or_default();
115
116        if !headers.contains_key("User-Agent") {
117            headers.insert("User-Agent", HeaderValue::from_static(MACH_USER_AGENT));
118        }
119
120        let host = uri.host().context("uri is missing a host")?.to_string();
121        let host_with_port = format!(
122            "{}:{}",
123            host,
124            uri.port_u16().unwrap_or_else(|| {
125                if matches!(uri.scheme_str(), Some("http") | None) {
126                    80
127                } else {
128                    443
129                }
130            })
131        );
132        debug!("host: {host_with_port}");
133
134        let method = match self.direction {
135            Direction::Down => "GET",
136            Direction::Up(_) => "POST",
137        };
138
139        let (tx, rx) = oneshot_result();
140        let mut events = None;
141        // Lets the request task report a failure the upload body cannot see
142        // itself -- notably a non-success response status. Must be dropped once
143        // the request finishes so the body remains the sole sender and its
144        // channel still closes when the transfer dies.
145        let mut upload_events_tx = None;
146
147        let body: NqBody = match self.direction {
148            Direction::Up(size) => {
149                tracing::trace!("tracking upload body");
150                let dummy_body = UploadBody::new(size);
151
152                let (body, events_rx) =
153                    CountingBody::new(dummy_body, Duration::from_millis(50), Arc::clone(&time));
154                events = Some(events_rx);
155                upload_events_tx = Some(body.sender());
156
157                headers.insert("Content-Type", HeaderValue::from_static("text/plain"));
158
159                body.boxed()
160            }
161            Direction::Down => {
162                tracing::debug!("created empty download body");
163                empty().boxed()
164            }
165        };
166
167        if let Some(scoped_headers) = self.scoped_headers.take() {
168            scoped_headers.apply(&uri, &mut headers);
169        }
170
171        let mut request = http::Request::builder()
172            .method(method)
173            .uri(uri)
174            .body(body)?;
175
176        *request.headers_mut() = headers.clone();
177        tracing::debug!("created request: {request:?}");
178
179        let failure_time = Arc::clone(&time);
180        let teardown = shutdown.clone();
181        tokio::spawn(
182            async move {
183                if let Err(error) = self
184                    .send_request(
185                        network,
186                        time,
187                        shutdown,
188                        headers,
189                        host,
190                        host_with_port,
191                        tx,
192                        events,
193                        request,
194                    )
195                    .await
196                {
197                    // Test teardown closes every connection while requests may still be in flight
198                    if teardown.is_cancelled() {
199                        debug!("ThroughputClient request cancelled by shutdown: {error:#}");
200                    } else {
201                        error!("error sending ThroughputClient request: {error:#}");
202                    }
203
204                    // An upload's failure (rejected status, reset stream, dead
205                    // connection) is invisible to its request body, which just
206                    // stops being polled. Report it explicitly so the transfer
207                    // is retired rather than appearing to run forever.
208                    if let Some(sender) = &upload_events_tx {
209                        let _ = sender.send(BodyEvent::Failed {
210                            at: failure_time.now(),
211                            reason: format!("{error:#}"),
212                        });
213                    }
214                }
215                // Drop the sender clone so the body is again the only owner of
216                // the events channel.
217                drop(upload_events_tx);
218            }
219            .in_current_span(),
220        );
221
222        Ok(rx)
223    }
224
225    #[allow(clippy::too_many_arguments)]
226    async fn send_request(
227        mut self,
228        network: Arc<dyn Network>,
229        time: Arc<dyn Time>,
230        shutdown: CancellationToken,
231        headers: HeaderMap,
232        host: String,
233        host_with_port: String,
234        tx: tokio::sync::oneshot::Sender<Result<InflightBody, anyhow::Error>>,
235        events: Option<mpsc::UnboundedReceiver<BodyEvent>>,
236        request: http::Request<http_body_util::combinators::BoxBody<Bytes, Infallible>>,
237    ) -> Result<Result<(), anyhow::Error>, anyhow::Error> {
238        let start = time.now();
239        let connection = self
240            .get_or_create_connection(&network, &time, host, host_with_port)
241            .await?;
242        let conn_timing = {
243            let conn = connection.read().await;
244            conn.timing()
245        };
246
247        debug!("sending request");
248        let response_fut = network.send_request(connection.clone(), request);
249
250        let response_body = self
251            .create_response_body(
252                time,
253                headers,
254                tx,
255                events,
256                start,
257                connection,
258                conn_timing,
259                response_fut,
260            )
261            .await
262            .context("creating response body")?;
263
264        tokio::spawn(consume_body(shutdown, response_body).in_current_span());
265
266        Ok(Ok::<_, anyhow::Error>(()))
267    }
268
269    #[allow(clippy::too_many_arguments)]
270    async fn create_response_body(
271        &self,
272        time: Arc<dyn Time>,
273        headers: HeaderMap,
274        tx: tokio::sync::oneshot::Sender<Result<InflightBody, anyhow::Error>>,
275        events: Option<mpsc::UnboundedReceiver<BodyEvent>>,
276        start: Timestamp,
277        connection: Arc<RwLock<EstablishedConnection>>,
278        conn_timing: crate::nq_core::ConnectionTiming,
279        response_fut: OneshotResult<http::Response<Incoming>>,
280    ) -> Result<http_body_util::combinators::BoxBody<Bytes, hyper::Error>, anyhow::Error> {
281        let response_body = match self.direction {
282            Direction::Up(_) => {
283                trace!("sending upload events");
284                if tx
285                    .send(Ok(InflightBody {
286                        connection: connection.clone(),
287                        timing: Some(conn_timing),
288                        events: events.expect("events were set above"),
289                        start,
290                        headers,
291                    }))
292                    .is_err()
293                {
294                    error!("error sending upload events");
295                }
296
297                let (parts, incoming) = response_fut
298                    .await
299                    .context("waiting for response")?
300                    .into_parts();
301                info!("upload response parts: {:?}", parts);
302
303                // A rejected upload (e.g. HTTP 413 when the body exceeds the
304                // server's buffering cap) is a perfectly well-formed HTTP
305                // response, so nothing below would otherwise notice: the load
306                // would be counted as healthy while transferring nothing.
307                if !parts.status.is_success() {
308                    bail!("upload rejected with status {}", parts.status);
309                }
310
311                incoming.boxed()
312            }
313            Direction::Down => {
314                let (parts, incoming) = response_fut.await?.into_parts();
315                info!("download response parts: {:?}", parts);
316
317                // The response is awaited before the caller is handed its
318                // `InflightBody`, so a bad status can be reported through the
319                // oneshot and the load never starts.
320                if !parts.status.is_success() {
321                    let reason = format!("download rejected with status {}", parts.status);
322                    let _ = tx.send(Err(anyhow::anyhow!("{reason}")));
323                    bail!(reason);
324                }
325
326                let (counting_body, events) =
327                    CountingBody::new(incoming, Duration::from_millis(100), Arc::clone(&time));
328
329                debug!("sending download events");
330                if tx
331                    .send(Ok(InflightBody {
332                        connection: connection.clone(),
333                        timing: Some(conn_timing),
334                        start,
335                        events,
336                        headers: parts.headers,
337                    }))
338                    .is_err()
339                {
340                    error!("error sending download events");
341                }
342
343                counting_body.boxed()
344            }
345        };
346        Ok(response_body)
347    }
348
349    async fn get_or_create_connection(
350        &mut self,
351        network: &Arc<dyn Network>,
352        time: &Arc<dyn Time>,
353        host: String,
354        host_with_port: String,
355    ) -> Result<Arc<RwLock<EstablishedConnection>>, anyhow::Error> {
356        let connection = if let Some(connection) = self.connection.take() {
357            connection
358        } else if let Some(conn_type) = self.new_connection_type {
359            info!("creating new connection to {host_with_port}");
360
361            let addrs = network
362                .resolve(host_with_port)
363                .await
364                .context("unable to resolve host")?;
365
366            debug!("addrs: {addrs:?}");
367
368            // Start the connection timing *after* DNS resolution so that
369            // tcp_f (draft-ietf-ippm-responsiveness-09 ยง5.3) measures the TCP
370            // handshake alone, without folding in the DNS lookup time.
371            let connect_start = time.now();
372
373            network
374                .new_connection(connect_start, addrs[0], host, conn_type)
375                .await
376                .context("creating new connection")?
377        } else {
378            todo!()
379        };
380
381        Ok(connection)
382    }
383}
384
385async fn consume_body(
386    shutdown: CancellationToken,
387    mut response_body: http_body_util::combinators::BoxBody<Bytes, hyper::Error>,
388) {
389    // Consume the response body and keep the connection alive. Stop if we hit an error.
390    info!("waiting for response body");
391    loop {
392        select! {
393            res = response_body.frame() => match res {
394                Some(Ok(_)) => {
395                    // Continue consuming frames
396                },
397                Some(Err(e)) => {
398                    error!("body closing: {e}");
399                    break;
400                },
401                None => {
402                    // Body finished successfully
403                    debug!("response body finished");
404                    break;
405                }
406            },
407            _ = shutdown.cancelled() => break,
408        }
409    }
410}
411
412/// A [`Client`] is a simple client which sends a request and returns a response.
413///
414/// The connection timing, e.g. TCP/TLS overhead, will be inserted into the response
415/// if it exits.
416#[derive(Default)]
417pub struct Client {
418    connection: Option<Arc<RwLock<EstablishedConnection>>>,
419    new_connection_type: Option<ConnectionType>,
420    headers: Option<HeaderMap>,
421    scoped_headers: Option<ScopedHeaders>,
422    method: Option<String>,
423}
424
425impl Client {
426    /// Create a new connection for each request.
427    pub fn new_connection(mut self, conn_type: ConnectionType) -> Self {
428        self.new_connection_type = Some(conn_type);
429        self
430    }
431
432    /// Set the headers for the upload or download request.
433    pub fn headers(mut self, headers: HeaderMap<HeaderValue>) -> Self {
434        self.headers = Some(headers);
435        self
436    }
437
438    /// Set headers that are only attached when the request's host matches the
439    /// scope's allowlist. Headers set via [`Self::headers`] take precedence.
440    pub fn scoped_headers(mut self, scoped_headers: Option<ScopedHeaders>) -> Self {
441        self.scoped_headers = scoped_headers;
442        self
443    }
444
445    /// Set the method used by the client.
446    pub fn method(mut self, method: &str) -> Self {
447        self.method = Some(method.to_string());
448        self
449    }
450
451    /// Send a request to the given uri with the given body, timing how long it
452    /// took.
453    #[tracing::instrument(skip(self, body, network, time))]
454    pub fn send<B>(
455        self,
456        uri: Uri,
457        body: B,
458        network: Arc<dyn Network>,
459        time: Arc<dyn Time>,
460    ) -> anyhow::Result<OneshotResult<http::Response<Incoming>>>
461    where
462        B: Body<Data = Bytes, Error = Infallible> + Send + Sync + 'static,
463    {
464        let mut headers = self.headers.unwrap_or_default();
465
466        if !headers.contains_key("User-Agent") {
467            headers.insert("User-Agent", HeaderValue::from_static(MACH_USER_AGENT));
468        }
469
470        let host = uri.host().context("uri is missing a host")?.to_string();
471
472        let remote_addr = (host.as_str(), uri.port_u16().unwrap_or(443))
473            .to_socket_addrs()?
474            .next()
475            .context("could not resolve large download url")?;
476
477        let method: http::Method = self.method.as_deref().unwrap_or("GET").parse()?;
478
479        if let Some(scoped_headers) = self.scoped_headers {
480            scoped_headers.apply(&uri, &mut headers);
481        }
482
483        let mut request = http::Request::builder()
484            .method(method)
485            .uri(uri)
486            .body(body.boxed())?;
487
488        *request.headers_mut() = headers.clone();
489
490        debug!("sending request");
491
492        let (tx, rx) = oneshot_result();
493        tokio::spawn(
494            async move {
495                let start = time.now();
496
497                let connection = if let Some(connection) = self.connection {
498                    connection
499                } else if let Some(conn_type) = self.new_connection_type {
500                    info!("creating new connection");
501                    network
502                        .new_connection(start, remote_addr, host, conn_type)
503                        .await?
504                } else {
505                    todo!()
506                };
507
508                // todo(fisher): fine-grained send timings for requests
509                let mut response = network.send_request(connection.clone(), request).await?;
510
511                let timing = {
512                    let conn = connection.read().await;
513                    conn.timing()
514                };
515
516                debug!(?connection, "connection used");
517
518                response.extensions_mut().insert(timing);
519
520                if tx.send(Ok(response)).is_err() {
521                    error!("unable to send response");
522                }
523
524                Ok::<_, anyhow::Error>(())
525            }
526            .in_current_span(),
527        );
528
529        Ok(rx)
530    }
531}
532
533/// Consumes body events until the body is finished and returns
534/// the time at which the body finished.
535pub async fn wait_for_finish(
536    mut body_events: mpsc::UnboundedReceiver<BodyEvent>,
537) -> anyhow::Result<FinishResult> {
538    let mut body_total = 0;
539
540    while let Some(event) = body_events.recv().await {
541        match event {
542            BodyEvent::ByteCount { total, .. } => body_total = total,
543            BodyEvent::Finished { at } => {
544                return Ok(FinishResult {
545                    total: body_total,
546                    finished_at: at,
547                });
548            }
549            BodyEvent::Failed { reason, .. } => {
550                return Err(anyhow::anyhow!(
551                    "body failed after {body_total} bytes: {reason}"
552                ));
553            }
554        }
555    }
556
557    Err(anyhow::anyhow!("body did not finish"))
558}
559
560/// The result of [`wait_for_finish`]
561#[derive(Debug)]
562pub struct FinishResult {
563    /// The total number of bytes seen by the body.
564    pub total: usize,
565    /// When the body finished.
566    pub finished_at: Timestamp,
567}