1use std::{collections::HashMap, sync::Arc};
5
6use crate::nq_core::client::{Direction, ThroughputClient};
7use crate::nq_core::{
8 BodyEvent, ConnectionType, EstablishedConnection, InflightBody, Network, OneshotResult,
9 ScopedHeaders, Time, Timestamp, oneshot_result,
10};
11use crate::nq_stats::CounterSeries;
12use anyhow::Context;
13use http::{HeaderMap, HeaderName, HeaderValue, Uri};
14use rand::seq::SliceRandom;
15use serde::Deserialize;
16use tokio::sync::RwLock;
17use tokio::sync::mpsc;
18use tokio::sync::mpsc::UnboundedReceiver;
19use tokio::sync::mpsc::error::TryRecvError;
20use tokio_util::sync::CancellationToken;
21use tracing::Instrument;
22
23#[derive(Debug, Deserialize)]
24pub struct LoadConfig {
25 pub headers: HashMap<String, String>,
26 #[serde(skip)]
29 pub scoped_headers: Option<ScopedHeaders>,
30 pub download_url: url::Url,
31 pub upload_url: url::Url,
32}
33
34pub struct LoadGenerator {
35 headers: HeaderMap<HeaderValue>,
36 scoped_headers: Option<ScopedHeaders>,
37 config: LoadConfig,
38 loads: Vec<LoadedConnection>,
39}
40
41impl LoadGenerator {
42 pub fn new(config: LoadConfig) -> anyhow::Result<Self> {
43 let mut headers = HeaderMap::new();
44
45 for (key, value) in config.headers.iter() {
46 headers.insert(
47 HeaderName::from_bytes(key.as_bytes())?,
48 HeaderValue::from_bytes(value.as_bytes())?,
49 );
50 }
51
52 Ok(Self {
53 headers,
54 scoped_headers: config.scoped_headers.clone(),
55 config,
56 loads: Vec::new(),
57 })
58 }
59
60 #[tracing::instrument(skip(self, network, time, shutdown))]
61 pub fn new_loaded_connection(
62 &self,
63 direction: Direction,
64 conn_type: ConnectionType,
65 network: Arc<dyn Network>,
66 time: Arc<dyn Time>,
67 shutdown: CancellationToken,
68 ) -> anyhow::Result<OneshotResult<LoadedConnection>> {
69 let (tx, rx) = oneshot_result();
70
71 let uri: Uri = match direction {
72 Direction::Up(_) => self.config.upload_url.as_str().parse()?,
73 Direction::Down => self.config.download_url.as_str().parse()?,
74 };
75
76 let client = match direction {
77 Direction::Down => ThroughputClient::download(),
78 Direction::Up(size) => ThroughputClient::upload(size),
79 };
80
81 let client = client
82 .new_connection(conn_type)
83 .headers(self.headers.clone())
84 .scoped_headers(self.scoped_headers.clone());
85
86 let response_fut = client.send(
87 uri.clone(),
88 Arc::clone(&network),
89 Arc::clone(&time),
90 shutdown.clone(),
91 )?;
92
93 tracing::debug!("got loaded connection response future");
94
95 let reissue = match direction {
99 Direction::Up(bound) => Some(UploadReissue {
100 bound,
101 uri,
102 headers: self.headers.clone(),
103 scoped_headers: self.scoped_headers.clone(),
104 network,
105 time,
106 shutdown,
107 }),
108 Direction::Down => None,
109 };
110
111 tokio::spawn(
112 async move {
113 let inflight_body = response_fut
114 .await
115 .context("could not await response for loaded connection")?;
116
117 tracing::debug!("sending loaded connection");
118
119 let Some(reissue) = reissue else {
120 let _ = tx.send(Ok(LoadedConnection {
121 connection: inflight_body.connection,
122 events_rx: inflight_body.events,
123 state: LoadState::default(),
124 }));
125
126 return Ok(());
127 };
128
129 let (events_tx, events_rx) = mpsc::unbounded_channel();
130 let connection = Arc::clone(&inflight_body.connection);
131
132 let _ = tx.send(Ok(LoadedConnection {
133 connection: Arc::clone(&connection),
134 events_rx,
135 state: LoadState::default(),
136 }));
137
138 reissue
139 .run(connection, inflight_body.events, events_tx)
140 .await;
141
142 Ok::<_, anyhow::Error>(())
143 }
144 .in_current_span(),
145 );
146
147 Ok(rx)
148 }
149
150 pub fn connections(&self) -> impl Iterator<Item = &LoadedConnection> {
151 self.loads.iter()
152 }
153
154 pub fn random_connection(&self) -> Option<Arc<RwLock<EstablishedConnection>>> {
155 let loads: Vec<_> = self.ongoing_loads().collect();
156 loads
157 .choose(&mut rand::thread_rng())
158 .map(|c| c.connection.clone())
159 }
160
161 pub fn push(&mut self, loaded_connection: LoadedConnection) {
162 self.loads.push(loaded_connection);
163 }
164
165 pub fn update(&mut self) {
166 for load in &mut self.loads {
167 load.update();
168 }
169 }
170
171 pub fn ongoing_loads(&self) -> impl Iterator<Item = &LoadedConnection> {
176 self.loads.iter().filter(|load| load.is_ongoing())
177 }
178
179 pub fn count_loads(&self) -> usize {
180 self.ongoing_loads().count()
181 }
182
183 pub fn count_failed_loads(&self) -> usize {
186 self.loads.iter().filter(|load| load.has_failed()).count()
187 }
188
189 pub fn into_connections(self) -> Vec<LoadedConnection> {
190 self.loads
191 }
192}
193
194struct UploadReissue {
208 bound: usize,
210 uri: Uri,
211 headers: HeaderMap<HeaderValue>,
212 scoped_headers: Option<ScopedHeaders>,
213 network: Arc<dyn Network>,
214 time: Arc<dyn Time>,
215 shutdown: CancellationToken,
216}
217
218#[derive(Debug, PartialEq, Eq)]
220enum RequestEnd {
221 Finished,
223 Died,
225}
226
227impl UploadReissue {
228 async fn run(
231 self,
232 connection: Arc<RwLock<EstablishedConnection>>,
233 first: UnboundedReceiver<BodyEvent>,
234 events_tx: mpsc::UnboundedSender<BodyEvent>,
235 ) {
236 let mut current = first;
237 let mut relay = CumulativeRelay::default();
238 let mut requests = 1usize;
239
240 loop {
241 let ended = loop {
242 let event = tokio::select! {
243 _ = self.shutdown.cancelled() => return,
248 event = current.recv() => event,
249 };
250
251 let Some(event) = event else {
252 break RequestEnd::Died;
253 };
254
255 match relay.on_event(event) {
256 RelayAction::Forward(event) => {
257 if events_tx.send(event).is_err() {
262 return;
263 }
264 }
265 RelayAction::RequestFinished => break RequestEnd::Finished,
266 RelayAction::Fail(event) => {
267 let _ = events_tx.send(event);
268 return;
269 }
270 }
271 };
272
273 if ended == RequestEnd::Died {
274 let _ = events_tx.send(BodyEvent::Failed {
275 at: self.time.now(),
276 reason: format!(
277 "upload terminated early after {} request(s), {} bytes",
278 requests,
279 relay.total()
280 ),
281 });
282 return;
283 }
284
285 if events_tx.is_closed() {
286 return;
287 }
288
289 let next = match self.issue(&connection) {
295 Ok(next) => next,
296 Err(error) => {
297 let _ = events_tx.send(BodyEvent::Failed {
298 at: self.time.now(),
299 reason: format!("could not start upload request {requests}: {error:#}"),
300 });
301 return;
302 }
303 };
304
305 let next = match next.await {
306 Ok(inflight) => inflight.events,
307 Err(error) => {
308 let _ = events_tx.send(BodyEvent::Failed {
309 at: self.time.now(),
310 reason: format!("upload request {requests} failed to start: {error:#}"),
311 });
312 return;
313 }
314 };
315
316 requests += 1;
317 tracing::debug!(
318 requests,
319 total_bytes = relay.total(),
320 "re-issued bounded upload request"
321 );
322
323 let finished = std::mem::replace(&mut current, next);
327 tokio::spawn(watch_tail(finished, events_tx.clone()).in_current_span());
328 }
329 }
330
331 fn issue(
332 &self,
333 connection: &Arc<RwLock<EstablishedConnection>>,
334 ) -> anyhow::Result<OneshotResult<InflightBody>> {
335 ThroughputClient::upload(self.bound)
336 .with_connection(Arc::clone(connection))
337 .headers(self.headers.clone())
338 .scoped_headers(self.scoped_headers.clone())
339 .send(
340 self.uri.clone(),
341 Arc::clone(&self.network),
342 Arc::clone(&self.time),
343 self.shutdown.clone(),
344 )
345 }
346}
347
348async fn watch_tail(
357 mut events: UnboundedReceiver<BodyEvent>,
358 events_tx: mpsc::UnboundedSender<BodyEvent>,
359) {
360 while let Some(event) = events.recv().await {
361 if matches!(event, BodyEvent::Failed { .. }) {
362 let _ = events_tx.send(event);
363 return;
364 }
365 }
366}
367
368#[derive(Debug, Default)]
377struct CumulativeRelay {
378 base: usize,
380 last: usize,
382}
383
384#[derive(Debug)]
386enum RelayAction {
387 Forward(BodyEvent),
389 RequestFinished,
393 Fail(BodyEvent),
395}
396
397impl CumulativeRelay {
398 fn on_event(&mut self, event: BodyEvent) -> RelayAction {
399 match event {
400 BodyEvent::ByteCount { at, total } => {
401 self.last = total;
402 RelayAction::Forward(BodyEvent::ByteCount {
403 at,
404 total: self.base + total,
405 })
406 }
407 BodyEvent::Finished { .. } => {
408 self.base += self.last;
409 self.last = 0;
410 RelayAction::RequestFinished
411 }
412 BodyEvent::Failed { at, reason } => RelayAction::Fail(BodyEvent::Failed { at, reason }),
413 }
414 }
415
416 fn total(&self) -> usize {
418 self.base + self.last
419 }
420}
421
422#[derive(Debug, Default)]
427struct LoadState {
428 total_bytes_series: CounterSeries,
429 finished_at: Option<Timestamp>,
430 failed: bool,
433 failure_reason: Option<String>,
436 stopping: bool,
439}
440
441impl LoadState {
442 fn apply(&mut self, event: BodyEvent) {
443 match event {
444 BodyEvent::ByteCount { at, total } => self.total_bytes_series.add(at, total as f64),
445 BodyEvent::Finished { at } => self.finished_at = Some(at),
446 BodyEvent::Failed { reason, .. } => {
447 self.failed = true;
448 self.failure_reason = Some(reason);
449 }
450 }
451 }
452
453 fn on_disconnected(&mut self) {
464 if self.finished_at.is_none() && !self.stopping {
465 self.failed = true;
466 }
467 }
468
469 fn is_ongoing(&self) -> bool {
476 self.finished_at.is_none() && !self.failed
477 }
478
479 fn drain(&mut self, events_rx: &mut UnboundedReceiver<BodyEvent>) {
486 loop {
487 match events_rx.try_recv() {
488 Ok(event) => self.apply(event),
489 Err(TryRecvError::Empty) => break,
490 Err(TryRecvError::Disconnected) => {
491 self.on_disconnected();
492 break;
493 }
494 }
495 }
496 }
497}
498
499#[derive(Debug)]
500pub struct LoadedConnection {
501 connection: Arc<RwLock<EstablishedConnection>>,
502 events_rx: UnboundedReceiver<BodyEvent>,
503 state: LoadState,
504}
505
506impl LoadedConnection {
507 pub fn update(&mut self) {
508 self.state.drain(&mut self.events_rx);
509 }
510
511 pub fn total_bytes_series(&self) -> &CounterSeries {
512 &self.state.total_bytes_series
513 }
514
515 pub fn is_ongoing(&self) -> bool {
517 self.state.is_ongoing()
518 }
519
520 pub fn has_failed(&self) -> bool {
522 self.state.failed
523 }
524
525 pub fn failure_reason(&self) -> Option<&str> {
527 self.state.failure_reason.as_deref()
528 }
529
530 pub fn stop(&mut self) {
531 self.state.stopping = true;
532 self.events_rx.close();
533 self.update();
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use std::time::Duration;
541 use tokio::sync::mpsc;
542
543 fn channel() -> (
544 mpsc::UnboundedSender<BodyEvent>,
545 mpsc::UnboundedReceiver<BodyEvent>,
546 ) {
547 mpsc::unbounded_channel()
548 }
549
550 fn forward_bytes(relay: &mut CumulativeRelay, at: Timestamp, total: usize) -> usize {
552 match relay.on_event(BodyEvent::ByteCount { at, total }) {
553 RelayAction::Forward(BodyEvent::ByteCount { total, .. }) => total,
554 other => panic!("a ByteCount must be forwarded, got {other:?}"),
555 }
556 }
557
558 #[test]
559 fn totals_accumulate_across_request_boundaries() {
560 let at = Timestamp::now();
561 let mut relay = CumulativeRelay::default();
562
563 assert_eq!(forward_bytes(&mut relay, at, 40), 40);
564 assert_eq!(forward_bytes(&mut relay, at, 100), 100);
565 relay.on_event(BodyEvent::Finished { at });
566
567 assert_eq!(forward_bytes(&mut relay, at, 0), 100);
570 assert_eq!(forward_bytes(&mut relay, at, 30), 130);
571 relay.on_event(BodyEvent::Finished { at });
572
573 assert_eq!(forward_bytes(&mut relay, at, 5), 135);
574 assert_eq!(relay.total(), 135);
575 }
576
577 #[test]
578 fn request_finished_is_never_forwarded() {
579 let at = Timestamp::now();
582 let mut relay = CumulativeRelay::default();
583
584 assert!(matches!(
585 relay.on_event(BodyEvent::Finished { at }),
586 RelayAction::RequestFinished
587 ));
588 }
589
590 #[test]
591 fn failure_is_terminal_and_forwarded() {
592 let at = Timestamp::now();
593 let mut relay = CumulativeRelay::default();
594
595 let action = relay.on_event(BodyEvent::Failed {
596 at,
597 reason: "upload rejected with status 413 Payload Too Large".to_owned(),
598 });
599
600 match action {
601 RelayAction::Fail(BodyEvent::Failed { reason, .. }) => {
602 assert!(reason.contains("413"));
603 }
604 other => panic!("a Failed must be forwarded as terminal, got {other:?}"),
605 }
606 }
607
608 #[test]
614 fn relayed_totals_never_produce_negative_goodput() {
615 let start = Timestamp::now();
616 let step = Duration::from_millis(50);
617
618 let mut relay = CumulativeRelay::default();
619 let mut series = CounterSeries::default();
620 let mut at = start;
621
622 for _ in 0..3 {
624 for total in [0usize, 25, 50, 75, 100] {
625 at = at + step;
626 let forwarded = forward_bytes(&mut relay, at, total);
627 series.add(at, forwarded as f64);
628 }
629 at = at + step;
630 relay.on_event(BodyEvent::Finished { at });
631 }
632
633 assert_eq!(relay.total(), 300, "three 100-byte requests");
634
635 let mut window = start;
636 while window < at {
637 let next = window + step;
638 let bytes = series.interval_sum(window, next);
639 assert!(
640 bytes >= 0.0,
641 "negative goodput ({bytes}) in one window -- a request boundary leaked a reset"
642 );
643 window = next;
644 }
645
646 assert_eq!(
647 series.interval_sum(start, at),
648 300.0,
649 "the whole run must account for every byte exactly once"
650 );
651 }
652
653 #[test]
654 fn open_channel_leaves_transfer_ongoing() {
655 let (tx, mut rx) = channel();
656 tx.send(BodyEvent::ByteCount {
657 at: Timestamp::now(),
658 total: 1024,
659 })
660 .unwrap();
661
662 let mut state = LoadState::default();
663 state.drain(&mut rx);
664
665 assert!(state.is_ongoing());
666 assert!(!state.failed);
667 drop(tx);
669 }
670
671 #[test]
672 fn disconnect_without_finished_marks_failed() {
673 let (tx, mut rx) = channel();
674 tx.send(BodyEvent::ByteCount {
675 at: Timestamp::now(),
676 total: 10 * 1024 * 1024,
677 })
678 .unwrap();
679 drop(tx);
682
683 let mut state = LoadState::default();
684 state.drain(&mut rx);
685
686 assert!(state.failed, "early termination must be flagged");
687 assert!(!state.is_ongoing(), "a failed load must not stay ongoing");
688 }
689
690 #[test]
691 fn finished_then_disconnect_is_not_a_failure() {
692 let (tx, mut rx) = channel();
693 let at = Timestamp::now();
694 tx.send(BodyEvent::ByteCount { at, total: 512 }).unwrap();
695 tx.send(BodyEvent::Finished { at }).unwrap();
696 drop(tx);
699
700 let mut state = LoadState::default();
701 state.drain(&mut rx);
702
703 assert!(!state.failed, "a completed transfer must not be a failure");
704 assert_eq!(state.finished_at, Some(at));
705 assert!(!state.is_ongoing(), "a completed load is no longer ongoing");
706 }
707
708 #[test]
709 fn teardown_disconnect_is_not_a_failure() {
710 let (tx, mut rx) = channel();
713 drop(tx);
714
715 let mut state = LoadState::default();
716 state.stopping = true;
717 state.drain(&mut rx);
718
719 assert!(!state.failed, "teardown must not be flagged as a failure");
720 }
721
722 #[test]
723 fn explicit_failed_event_retires_the_load_with_a_reason() {
724 let (tx, mut rx) = channel();
727 let at = Timestamp::now();
728 tx.send(BodyEvent::ByteCount { at, total: 1024 }).unwrap();
729 tx.send(BodyEvent::Failed {
730 at,
731 reason: "upload rejected with status 413 Payload Too Large".to_owned(),
732 })
733 .unwrap();
734
735 let mut state = LoadState::default();
736 state.drain(&mut rx);
737
738 assert!(state.failed);
739 assert!(!state.is_ongoing());
740 assert_eq!(
741 state.failure_reason.as_deref(),
742 Some("upload rejected with status 413 Payload Too Large")
743 );
744 drop(tx);
747 }
748
749 #[test]
750 fn bytes_seen_before_failure_are_retained() {
751 let (tx, mut rx) = channel();
754 let at = Timestamp::now();
755 tx.send(BodyEvent::ByteCount { at, total: 4096 }).unwrap();
756 drop(tx);
757
758 let mut state = LoadState::default();
759 state.drain(&mut rx);
760
761 assert!(state.failed);
762 assert_eq!(state.total_bytes_series.sum(), 4096.0);
763 }
764}