1use 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
27pub const MACH_USER_AGENT: &str = concat!("mach/", env!("CARGO_PKG_VERSION"));
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Direction {
34 Down,
36 Up(usize),
38}
39
40pub 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 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 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 pub fn with_connection(mut self, connection: Arc<RwLock<EstablishedConnection>>) -> Self {
81 self.connection = Some(connection);
82 self
83 }
84
85 pub fn new_connection(mut self, conn_type: ConnectionType) -> Self {
87 self.new_connection_type = Some(conn_type);
88 self
89 }
90
91 pub fn headers(mut self, headers: HeaderMap<HeaderValue>) -> Self {
93 self.headers = Some(headers);
94 self
95 }
96
97 pub fn scoped_headers(mut self, scoped_headers: Option<ScopedHeaders>) -> Self {
100 self.scoped_headers = scoped_headers;
101 self
102 }
103
104 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 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 if teardown.is_cancelled() {
199 debug!("ThroughputClient request cancelled by shutdown: {error:#}");
200 } else {
201 error!("error sending ThroughputClient request: {error:#}");
202 }
203
204 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(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 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 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 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 info!("waiting for response body");
391 loop {
392 select! {
393 res = response_body.frame() => match res {
394 Some(Ok(_)) => {
395 },
397 Some(Err(e)) => {
398 error!("body closing: {e}");
399 break;
400 },
401 None => {
402 debug!("response body finished");
404 break;
405 }
406 },
407 _ = shutdown.cancelled() => break,
408 }
409 }
410}
411
412#[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 pub fn new_connection(mut self, conn_type: ConnectionType) -> Self {
428 self.new_connection_type = Some(conn_type);
429 self
430 }
431
432 pub fn headers(mut self, headers: HeaderMap<HeaderValue>) -> Self {
434 self.headers = Some(headers);
435 self
436 }
437
438 pub fn scoped_headers(mut self, scoped_headers: Option<ScopedHeaders>) -> Self {
441 self.scoped_headers = scoped_headers;
442 self
443 }
444
445 pub fn method(mut self, method: &str) -> Self {
447 self.method = Some(method.to_string());
448 self
449 }
450
451 #[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 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
533pub 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#[derive(Debug)]
562pub struct FinishResult {
563 pub total: usize,
565 pub finished_at: Timestamp,
567}