1use std::{
4 collections::HashSet,
5 sync::{
6 Arc,
7 atomic::{AtomicBool, AtomicU64, Ordering},
8 },
9 time::Duration,
10};
11
12use tokio::{
13 sync::Semaphore,
14 task::{JoinHandle, JoinSet},
15 time::{Instant, MissedTickBehavior},
16};
17
18use crate::{
19 Error, NetBenchFlow, NetBenchReceiveStream, NetBenchSendStream, NetBenchSession,
20 PROTOCOL_VERSION, Result,
21 config::{LOADED_LATENCY_INTERVAL, MAX_PROBE_RATE_PER_SECOND, THROUGHPUT_SAMPLE_INTERVAL},
22 wire::{
23 Capabilities, ControlMessage, ErrorCode, PROBE_MAGIC, Probe, ProbeKind, ServerLimits,
24 read_control, write_control,
25 },
26};
27
28const DEFAULT_MAX_CHUNK_SIZE: u32 = 1024 * 1024;
29const DOWNLOAD_STREAM_MAGIC: u8 = 0x44;
30const UPLOAD_STREAM_MAGIC: u8 = 0x55;
31const THROUGHPUT_SETUP_TIMEOUT: Duration = Duration::from_secs(5);
32const THROUGHPUT_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
33type SendStream = Box<dyn NetBenchSendStream>;
34type RecvStream = Box<dyn NetBenchReceiveStream>;
35
36struct ProbeEchoTask {
37 test_id: u64,
38 task: Option<JoinHandle<Result<()>>>,
39}
40
41impl ProbeEchoTask {
42 fn spawn(
43 session: Arc<dyn NetBenchSession>,
44 test_id: u64,
45 lifetime: Duration,
46 max_requests: u64,
47 ) -> Self {
48 Self {
49 test_id,
50 task: Some(tokio::spawn(echo_datagrams(
51 session,
52 test_id,
53 lifetime,
54 max_requests,
55 ))),
56 }
57 }
58
59 async fn stop(mut self) -> Result<()> {
60 let Some(task) = self.task.take() else {
61 return Ok(());
62 };
63 if !task.is_finished() {
64 task.abort();
65 }
66 match task.await {
67 Ok(result) => result,
68 Err(error) if error.is_cancelled() => Ok(()),
69 Err(error) => Err(Error::Protocol(format!(
70 "probe echo task ended unexpectedly: {error}"
71 ))),
72 }
73 }
74}
75
76impl Drop for ProbeEchoTask {
77 fn drop(&mut self) {
78 if let Some(task) = &self.task {
79 task.abort();
80 }
81 }
82}
83
84#[derive(Clone)]
85struct Connection(Arc<dyn NetBenchSession>);
86
87impl Connection {
88 fn max_datagram_size(&self) -> Option<usize> {
89 self.0.max_datagram_size()
90 }
91
92 async fn open_uni(&self) -> Result<SendStream> {
93 Ok(self.0.open_bi().await?.into_split().0)
94 }
95
96 async fn accept_uni(&self) -> Result<RecvStream> {
97 Ok(self.0.accept_bi().await?.into_split().1)
98 }
99}
100
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub enum ThroughputPolicy {
106 #[default]
108 Allow,
109 Deny,
111}
112
113#[derive(Clone)]
115pub struct NetBenchResponder {
116 concurrent_tests: usize,
117 test_duration: Duration,
118 parallel_streams: u16,
119 chunk_size: u32,
120 throughput_allowed: Arc<AtomicBool>,
121 permits: Arc<Semaphore>,
122}
123
124impl std::fmt::Debug for NetBenchResponder {
125 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 formatter
127 .debug_struct("NetBenchResponder")
128 .field("max_concurrent_tests", &self.concurrent_tests)
129 .field("max_test_duration", &self.test_duration)
130 .field("max_parallel_streams", &self.parallel_streams)
131 .field("throughput_policy", &self.throughput_policy())
132 .field("max_chunk_size", &self.chunk_size)
133 .finish_non_exhaustive()
134 }
135}
136
137impl NetBenchResponder {
138 #[must_use]
140 pub fn builder() -> NetBenchResponderBuilder {
141 NetBenchResponderBuilder::default()
142 }
143
144 #[must_use]
146 pub const fn max_concurrent_tests(&self) -> usize {
147 self.concurrent_tests
148 }
149
150 #[must_use]
152 pub const fn max_test_duration(&self) -> Duration {
153 self.test_duration
154 }
155
156 #[must_use]
158 pub const fn max_parallel_streams(&self) -> u16 {
159 self.parallel_streams
160 }
161
162 #[must_use]
164 pub fn throughput_policy(&self) -> ThroughputPolicy {
165 if self.throughput_allowed.load(Ordering::Relaxed) {
166 ThroughputPolicy::Allow
167 } else {
168 ThroughputPolicy::Deny
169 }
170 }
171
172 pub fn set_throughput_policy(&self, policy: ThroughputPolicy) {
176 self.throughput_allowed
177 .store(policy == ThroughputPolicy::Allow, Ordering::Relaxed);
178 }
179
180 fn limits(&self) -> ServerLimits {
181 ServerLimits {
182 test_duration_ms: duration_ms(self.test_duration),
183 parallel_streams: if self.throughput_policy() == ThroughputPolicy::Allow {
185 self.parallel_streams
186 } else {
187 0
188 },
189 chunk_size: self.chunk_size,
190 }
191 }
192
193 fn validate_phase(&self, duration_ms: u32) -> Result<Duration> {
194 let requested = Duration::from_millis(u64::from(duration_ms));
195 if requested.is_zero() || requested > self.test_duration {
196 return Err(Error::DurationLimitExceeded {
197 requested,
198 maximum: self.test_duration,
199 });
200 }
201 Ok(requested)
202 }
203
204 fn validate_throughput(
205 &self,
206 duration_ms: u32,
207 streams: u16,
208 chunk_size: u32,
209 ) -> Result<Duration> {
210 if self.throughput_policy() == ThroughputPolicy::Deny || self.parallel_streams == 0 {
211 return Err(Error::ThroughputDeniedByPeer);
212 }
213 let duration = self.validate_phase(duration_ms)?;
214 if streams == 0 || streams > self.parallel_streams {
215 return Err(Error::Protocol(format!(
216 "stream count {streams} is outside 1..={}",
217 self.parallel_streams
218 )));
219 }
220 if chunk_size == 0 || chunk_size > self.chunk_size {
221 return Err(Error::Protocol(format!(
222 "chunk size {chunk_size} is outside 1..={}",
223 self.chunk_size
224 )));
225 }
226 Ok(duration)
227 }
228
229 #[allow(
230 clippy::too_many_lines,
231 reason = "linear command dispatch is easier to audit"
232 )]
233 pub async fn serve(&self, flow: NetBenchFlow) -> Result<()> {
239 let (session, mut control_send, mut control_recv) = flow.into_parts();
240 let connection = Connection(Arc::clone(&session));
241 let permit = Arc::clone(&self.permits).try_acquire_owned();
242
243 let Ok(_permit) = permit else {
244 write_control(
245 &mut control_send,
246 &ControlMessage::Error {
247 code: ErrorCode::Busy,
248 message: "server concurrency limit reached".to_owned(),
249 },
250 )
251 .await?;
252 control_send.finish().map_err(Error::network)?;
253 return Ok(());
254 };
255
256 let hello = read_control(&mut control_recv).await?;
257 let ControlMessage::ClientHello {
258 protocol_versions,
259 capabilities: _,
260 } = hello
261 else {
262 return Err(Error::Protocol(
263 "first control message must be ClientHello".to_owned(),
264 ));
265 };
266 if !protocol_versions.contains(&PROTOCOL_VERSION) {
267 write_control(
268 &mut control_send,
269 &ControlMessage::Error {
270 code: ErrorCode::UnsupportedVersion,
271 message: "no mutually supported protocol version".to_owned(),
272 },
273 )
274 .await?;
275 control_send.finish().map_err(Error::network)?;
276 return Ok(());
277 }
278
279 write_control(
280 &mut control_send,
281 &ControlMessage::ServerHello {
282 selected_version: PROTOCOL_VERSION,
283 limits: self.limits(),
284 capabilities: Capabilities {
285 datagram_probes: connection.max_datagram_size().is_some(),
286 loaded_latency: true,
287 path_stats: true,
288 },
289 },
290 )
291 .await?;
292
293 let mut active_probe = None::<ProbeEchoTask>;
294
295 loop {
296 let message = read_control(&mut control_recv).await?;
297
298 let result = match message {
299 ControlMessage::StartLatency {
300 test_id,
301 duration_ms,
302 interval_ms,
303 } => self
304 .validate_phase(duration_ms)
305 .and_then(|_| latency_probe_budget(duration_ms, interval_ms))
306 .and_then(|max_requests| {
307 start_probe_echo(
308 &mut active_probe,
309 Arc::clone(&session),
310 test_id,
311 self.test_duration,
312 max_requests,
313 )
314 }),
315 ControlMessage::StartLoss {
316 test_id,
317 duration_ms,
318 rate_per_second,
319 timeout_ms: _,
320 } => self
321 .validate_phase(duration_ms)
322 .and_then(|_| loss_probe_budget(duration_ms, rate_per_second))
323 .and_then(|max_requests| {
324 start_probe_echo(
325 &mut active_probe,
326 Arc::clone(&session),
327 test_id,
328 self.test_duration,
329 max_requests,
330 )
331 }),
332 ControlMessage::StopTest { test_id } => match active_probe.take() {
333 None => Err(Error::Protocol(format!(
334 "received StopTest for inactive test {test_id}"
335 ))),
336 Some(probe) if probe.test_id != test_id => {
337 let active_test_id = probe.test_id;
338 active_probe = Some(probe);
339 Err(Error::Protocol(format!(
340 "received StopTest for test {test_id}, active test is {active_test_id}"
341 )))
342 }
343 Some(probe) => probe.stop().await,
344 },
345 ControlMessage::FlowFinished => {
346 if let Some(probe) = &active_probe {
347 Err(Error::Protocol(format!(
348 "flow finished while probe test {} is still active",
349 probe.test_id
350 )))
351 } else {
352 write_control(&mut control_send, &ControlMessage::FlowFinishedAck).await?;
353 break;
354 }
355 }
356 ControlMessage::StartDownload {
357 test_id,
358 duration_ms,
359 streams,
360 chunk_size,
361 } => {
362 if let Some(probe) = &active_probe {
363 let error = Error::Protocol(format!(
364 "cannot start download test {test_id} while probe test {} is active",
365 probe.test_id
366 ));
367 send_error_best_effort(
368 &mut control_send,
369 ErrorCode::InvalidRequest,
370 error.to_string(),
371 )
372 .await;
373 return Err(error);
374 }
375 let duration = match self.validate_throughput(duration_ms, streams, chunk_size)
376 {
377 Ok(duration) => duration,
378 Err(error) => {
379 send_error_best_effort(
380 &mut control_send,
381 ErrorCode::InvalidRequest,
382 error.to_string(),
383 )
384 .await;
385 return Err(error);
386 }
387 };
388 let phase_timeout = duration
389 .saturating_add(THROUGHPUT_SETUP_TIMEOUT)
390 .saturating_add(THROUGHPUT_CLEANUP_TIMEOUT);
391 tokio::time::timeout(phase_timeout, async {
392 let download_streams =
393 open_download_streams(connection.clone(), streams).await?;
394 write_control(&mut control_send, &ControlMessage::TestReady { test_id })
395 .await?;
396 expect_test_ready(&mut control_recv, test_id).await?;
397 let probe = ProbeEchoTask::spawn(
398 Arc::clone(&session),
399 test_id,
400 duration.saturating_add(THROUGHPUT_CLEANUP_TIMEOUT),
401 loaded_probe_budget(duration),
402 );
403 let download = send_download(
404 download_streams,
405 duration,
406 usize::try_from(chunk_size).map_err(Error::network)?,
407 )
408 .await;
409 let probe_result = probe.stop().await;
410 let (bytes, elapsed) = download?;
411 probe_result?;
412 tracing::debug!(
413 test_id,
414 direction = "download",
415 received_bytes = bytes,
416 measurement_duration = ?elapsed,
417 "throughput data tasks completed; queueing completion on the prioritized control stream"
418 );
419 write_control(
420 &mut control_send,
421 &ControlMessage::TestFinished {
422 test_id,
423 received_bytes: bytes,
424 duration_ns: duration_ns(elapsed),
425 },
426 )
427 .await
428 })
429 .await
430 .map_err(|_| Error::Timeout {
431 stage: "download phase",
432 })?
433 }
434 ControlMessage::StartUpload {
435 test_id,
436 duration_ms,
437 streams,
438 chunk_size,
439 } => {
440 if let Some(probe) = &active_probe {
441 let error = Error::Protocol(format!(
442 "cannot start upload test {test_id} while probe test {} is active",
443 probe.test_id
444 ));
445 send_error_best_effort(
446 &mut control_send,
447 ErrorCode::InvalidRequest,
448 error.to_string(),
449 )
450 .await;
451 return Err(error);
452 }
453 let duration = match self.validate_throughput(duration_ms, streams, chunk_size)
454 {
455 Ok(duration) => duration,
456 Err(error) => {
457 send_error_best_effort(
458 &mut control_send,
459 ErrorCode::InvalidRequest,
460 error.to_string(),
461 )
462 .await;
463 return Err(error);
464 }
465 };
466 let phase_timeout = duration
467 .saturating_add(THROUGHPUT_SETUP_TIMEOUT)
468 .saturating_add(THROUGHPUT_CLEANUP_TIMEOUT);
469 tokio::time::timeout(phase_timeout, async {
470 let upload_streams =
471 accept_upload_streams(connection.clone(), streams).await?;
472 write_control(&mut control_send, &ControlMessage::TestReady { test_id })
473 .await?;
474 let probe = ProbeEchoTask::spawn(
475 Arc::clone(&session),
476 test_id,
477 duration.saturating_add(THROUGHPUT_CLEANUP_TIMEOUT),
478 loaded_probe_budget(duration),
479 );
480 let upload =
481 receive_upload(upload_streams, &mut control_send, test_id, duration)
482 .await;
483 let probe_result = probe.stop().await;
484 let (bytes, elapsed) = upload?;
485 probe_result?;
486 tracing::debug!(
487 test_id,
488 direction = "upload",
489 received_bytes = bytes,
490 measurement_duration = ?elapsed,
491 "throughput data tasks completed; queueing completion on the prioritized control stream"
492 );
493 write_control(
494 &mut control_send,
495 &ControlMessage::TestFinished {
496 test_id,
497 received_bytes: bytes,
498 duration_ns: duration_ns(elapsed),
499 },
500 )
501 .await
502 })
503 .await
504 .map_err(|_| Error::Timeout {
505 stage: "upload phase",
506 })?
507 }
508 ControlMessage::ClientHello { .. }
509 | ControlMessage::ServerHello { .. }
510 | ControlMessage::TestReady { .. }
511 | ControlMessage::ThroughputProgress { .. }
512 | ControlMessage::TestFinished { .. }
513 | ControlMessage::FlowFinishedAck
514 | ControlMessage::Error { .. } => Err(Error::Protocol(
515 "message is invalid in server command state".to_owned(),
516 )),
517 };
518
519 if let Err(error) = result {
520 send_error_best_effort(
521 &mut control_send,
522 ErrorCode::InvalidRequest,
523 error.to_string(),
524 )
525 .await;
526 return Err(error);
527 }
528 }
529
530 Ok(())
531 }
532}
533
534impl Default for NetBenchResponder {
535 fn default() -> Self {
536 Self::builder().build()
537 }
538}
539
540#[derive(Clone)]
542pub struct NetBenchResponderBuilder {
543 concurrent_tests: usize,
544 test_duration: Duration,
545 parallel_streams: u16,
546 chunk_size: u32,
547 throughput_policy: ThroughputPolicy,
548}
549
550impl std::fmt::Debug for NetBenchResponderBuilder {
551 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 formatter
553 .debug_struct("NetBenchResponderBuilder")
554 .field("max_concurrent_tests", &self.concurrent_tests)
555 .field("max_test_duration", &self.test_duration)
556 .field("max_parallel_streams", &self.parallel_streams)
557 .field("throughput_policy", &self.throughput_policy)
558 .field("max_chunk_size", &self.chunk_size)
559 .finish()
560 }
561}
562
563impl NetBenchResponderBuilder {
564 #[must_use]
566 pub const fn max_concurrent_tests(mut self, value: usize) -> Self {
567 self.concurrent_tests = value;
568 self
569 }
570
571 #[must_use]
573 pub const fn max_test_duration(mut self, value: Duration) -> Self {
574 self.test_duration = value;
575 self
576 }
577
578 #[must_use]
580 pub const fn max_parallel_streams(mut self, value: u16) -> Self {
581 self.parallel_streams = value;
582 self
583 }
584
585 #[must_use]
587 pub const fn throughput_policy(mut self, value: ThroughputPolicy) -> Self {
588 self.throughput_policy = value;
589 self
590 }
591
592 #[must_use]
594 pub fn build(self) -> NetBenchResponder {
595 NetBenchResponder {
596 concurrent_tests: self.concurrent_tests,
597 test_duration: self.test_duration,
598 parallel_streams: self.parallel_streams,
599 chunk_size: self.chunk_size,
600 throughput_allowed: Arc::new(AtomicBool::new(matches!(
601 self.throughput_policy,
602 ThroughputPolicy::Allow
603 ))),
604 permits: Arc::new(Semaphore::new(self.concurrent_tests)),
605 }
606 }
607}
608
609impl Default for NetBenchResponderBuilder {
610 fn default() -> Self {
611 Self {
612 concurrent_tests: 2,
613 test_duration: Duration::from_secs(30),
614 parallel_streams: 8,
615 chunk_size: DEFAULT_MAX_CHUNK_SIZE,
616 throughput_policy: ThroughputPolicy::Allow,
617 }
618 }
619}
620
621fn start_probe_echo(
622 active: &mut Option<ProbeEchoTask>,
623 session: Arc<dyn NetBenchSession>,
624 test_id: u64,
625 lifetime: Duration,
626 max_requests: u64,
627) -> Result<()> {
628 if let Some(probe) = active {
629 return Err(Error::Protocol(format!(
630 "cannot start test {test_id} while probe test {} is active",
631 probe.test_id
632 )));
633 }
634 *active = Some(ProbeEchoTask::spawn(
635 session,
636 test_id,
637 lifetime,
638 max_requests,
639 ));
640 Ok(())
641}
642
643fn latency_probe_budget(duration_ms: u32, interval_ms: u32) -> Result<u64> {
644 if interval_ms == 0 {
645 return Err(Error::Protocol(
646 "latency probe interval must be at least one millisecond".to_owned(),
647 ));
648 }
649 Ok(u64::from(duration_ms).div_ceil(u64::from(interval_ms)))
650}
651
652fn loss_probe_budget(duration_ms: u32, rate_per_second: u32) -> Result<u64> {
653 if !(1..=MAX_PROBE_RATE_PER_SECOND).contains(&rate_per_second) {
654 return Err(Error::Protocol(format!(
655 "loss probe rate must be within 1..={MAX_PROBE_RATE_PER_SECOND} per second"
656 )));
657 }
658 Ok((u64::from(duration_ms) * u64::from(rate_per_second)).div_ceil(1_000))
659}
660
661fn loaded_probe_budget(duration: Duration) -> u64 {
662 let requests = duration
663 .as_nanos()
664 .div_ceil(LOADED_LATENCY_INTERVAL.as_nanos());
665 u64::try_from(requests).unwrap_or(u64::MAX)
666}
667
668async fn echo_datagrams(
669 session: Arc<dyn NetBenchSession>,
670 test_id: u64,
671 lifetime: Duration,
672 max_requests: u64,
673) -> Result<()> {
674 let deadline = Instant::now() + lifetime;
675 let mut echoed = HashSet::<u64>::new();
676 let mut echoed_count = 0_u64;
677 while echoed_count < max_requests {
678 let bytes = match tokio::time::timeout_at(deadline, session.read_datagram()).await {
679 Ok(result) => result?,
680 Err(_) => break,
681 };
682 let Ok(mut probe) = postcard::from_bytes::<Probe>(&bytes) else {
683 continue;
684 };
685 if probe.magic != PROBE_MAGIC
686 || probe.test_id != test_id
687 || probe.kind != ProbeKind::Request
688 || !echoed.insert(probe.sequence)
689 {
690 continue;
691 }
692 echoed_count += 1;
693 probe.kind = ProbeKind::Response;
694 let payload = postcard::to_allocvec(&probe)?;
695 match tokio::time::timeout_at(deadline, session.send_datagram(payload)).await {
696 Ok(result) => result?,
697 Err(_) => break,
698 }
699 }
700 Ok(())
701}
702
703async fn send_error_best_effort(control_send: &mut SendStream, code: ErrorCode, message: String) {
704 let _ = tokio::time::timeout(
705 THROUGHPUT_CLEANUP_TIMEOUT,
706 write_control(control_send, &ControlMessage::Error { code, message }),
707 )
708 .await;
709}
710
711async fn send_download(
712 streams: Vec<SendStream>,
713 duration: Duration,
714 chunk_size: usize,
715) -> Result<(u64, Duration)> {
716 let total = Arc::new(AtomicU64::new(0));
717 let started = Instant::now();
718 let deadline = started + duration;
719 let chunk = Arc::new(vec![0xA5; chunk_size]);
720 let mut tasks = JoinSet::new();
721
722 for mut stream in streams {
723 let total = Arc::clone(&total);
724 let chunk = Arc::clone(&chunk);
725 tasks.spawn(async move {
726 while Instant::now() < deadline {
727 match tokio::time::timeout_at(deadline, stream.write(&chunk)).await {
728 Ok(Ok(written)) => {
729 total.fetch_add(written as u64, Ordering::Relaxed);
730 }
731 Ok(Err(Error::FlowStopped)) | Err(_) => break,
732 Ok(Err(error)) => return Err(error),
733 }
734 }
735 stream.cancel();
736 Result::<()>::Ok(())
737 });
738 }
739
740 while let Some(result) = tasks.join_next().await {
741 result.map_err(Error::network)??;
742 }
743 tokio::time::sleep_until(deadline).await;
744 Ok((total.load(Ordering::Relaxed), duration))
745}
746
747async fn open_download_streams(connection: Connection, streams: u16) -> Result<Vec<SendStream>> {
748 let mut opened = Vec::with_capacity(usize::from(streams));
749 for _ in 0..streams {
750 let mut stream = connection.open_uni().await?;
751 stream.write_all(&[DOWNLOAD_STREAM_MAGIC]).await?;
752 opened.push(stream);
753 }
754 Ok(opened)
755}
756
757async fn expect_test_ready(recv: &mut RecvStream, expected_test_id: u64) -> Result<()> {
758 match read_control(recv).await? {
759 ControlMessage::TestReady { test_id } if test_id == expected_test_id => Ok(()),
760 ControlMessage::TestReady { test_id } => Err(Error::Protocol(format!(
761 "received readiness for test {test_id}, expected {expected_test_id}"
762 ))),
763 message => Err(Error::Protocol(format!(
764 "expected TestReady, received {message:?}"
765 ))),
766 }
767}
768
769async fn accept_upload_streams(connection: Connection, streams: u16) -> Result<Vec<RecvStream>> {
770 let mut accepted = Vec::with_capacity(usize::from(streams));
771 for _ in 0..streams {
772 let mut stream = connection.accept_uni().await?;
773 let mut magic = [0_u8; 1];
774 stream.read_exact(&mut magic).await?;
775 if magic[0] != UPLOAD_STREAM_MAGIC {
776 return Err(Error::Protocol(
777 "upload stream has an invalid pre-measurement header".to_owned(),
778 ));
779 }
780 accepted.push(stream);
781 }
782 Ok(accepted)
783}
784
785async fn receive_upload(
786 streams: Vec<RecvStream>,
787 control_send: &mut SendStream,
788 test_id: u64,
789 duration: Duration,
790) -> Result<(u64, Duration)> {
791 let total = Arc::new(AtomicU64::new(0));
792 let mut tasks = JoinSet::new();
793
794 let started = Instant::now();
795 let deadline = started + duration;
796 for mut stream in streams {
797 let total = Arc::clone(&total);
798 tasks.spawn(async move {
799 let mut buffer = vec![0_u8; 64 * 1024];
800 while Instant::now() < deadline {
801 match tokio::time::timeout_at(deadline, stream.read(&mut buffer)).await {
802 Ok(Ok(read)) if read > 0 && Instant::now() <= deadline => {
803 total.fetch_add(read as u64, Ordering::Relaxed);
804 }
805 Ok(Ok(_)) | Err(_) => break,
806 Ok(Err(error)) => return Err(error),
807 }
808 }
809 stream.cancel();
810 Result::<()>::Ok(())
811 });
812 }
813 let mut ticker = tokio::time::interval(THROUGHPUT_SAMPLE_INTERVAL);
814 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
815 ticker.tick().await;
816 while !tasks.is_empty() {
817 tokio::select! {
818 result = tasks.join_next() => {
819 if let Some(result) = result {
820 result.map_err(Error::network)??;
821 }
822 }
823 _ = ticker.tick() => {
824 tokio::time::timeout_at(
825 deadline + THROUGHPUT_CLEANUP_TIMEOUT,
826 write_control(
827 control_send,
828 &ControlMessage::ThroughputProgress {
829 test_id,
830 received_bytes: total.load(Ordering::Relaxed),
831 duration_ns: duration_ns(started.elapsed().min(duration)),
832 },
833 ),
834 )
835 .await
836 .map_err(|_| Error::Timeout {
837 stage: "upload progress",
838 })??;
839 }
840 }
841 }
842 tokio::time::sleep_until(deadline).await;
843 Ok((total.load(Ordering::Relaxed), duration))
844}
845
846fn duration_ms(duration: Duration) -> u32 {
847 u32::try_from(duration.as_millis()).unwrap_or(u32::MAX)
848}
849
850fn duration_ns(duration: Duration) -> u64 {
851 u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
852}
853
854#[cfg(test)]
855mod tests {
856 use std::{collections::VecDeque, future::pending, sync::Mutex};
857
858 use async_trait::async_trait;
859
860 use super::*;
861 use crate::{NetBenchBidirectionalStream, NetBenchTelemetry};
862
863 struct PeerStoppedSend;
864
865 #[async_trait]
866 impl NetBenchSendStream for PeerStoppedSend {
867 async fn write_all(&mut self, _bytes: &[u8]) -> Result<()> {
868 Err(Error::FlowStopped)
869 }
870
871 fn finish(&mut self) -> Result<()> {
872 Ok(())
873 }
874
875 fn cancel(&mut self) {}
876 }
877
878 #[derive(Default)]
879 struct ProbeSession {
880 datagrams: Mutex<VecDeque<Vec<u8>>>,
881 sent: Mutex<Vec<Vec<u8>>>,
882 }
883
884 #[async_trait]
885 impl NetBenchSession for ProbeSession {
886 fn remote_peer_id(&self) -> String {
887 "peer".to_owned()
888 }
889
890 fn telemetry(&self) -> NetBenchTelemetry {
891 NetBenchTelemetry::default()
892 }
893
894 fn max_datagram_size(&self) -> Option<usize> {
895 Some(1_200)
896 }
897
898 async fn open_bi(&self) -> Result<Box<dyn NetBenchBidirectionalStream>> {
899 Err(Error::Protocol("stream operation is unused".to_owned()))
900 }
901
902 async fn accept_bi(&self) -> Result<Box<dyn NetBenchBidirectionalStream>> {
903 Err(Error::Protocol("stream operation is unused".to_owned()))
904 }
905
906 async fn send_datagram(&self, bytes: Vec<u8>) -> Result<()> {
907 lock_unpoisoned(&self.sent).push(bytes);
908 Ok(())
909 }
910
911 async fn read_datagram(&self) -> Result<Vec<u8>> {
912 if let Some(bytes) = lock_unpoisoned(&self.datagrams).pop_front() {
913 return Ok(bytes);
914 }
915 pending().await
916 }
917 }
918
919 struct SinkSend;
920
921 #[async_trait]
922 impl NetBenchSendStream for SinkSend {
923 async fn write_all(&mut self, _bytes: &[u8]) -> Result<()> {
924 Ok(())
925 }
926
927 fn finish(&mut self) -> Result<()> {
928 Ok(())
929 }
930
931 fn cancel(&mut self) {}
932 }
933
934 struct ScriptedReceive {
935 bytes: VecDeque<u8>,
936 terminal: Option<Error>,
937 }
938
939 #[async_trait]
940 impl NetBenchReceiveStream for ScriptedReceive {
941 async fn read(&mut self, bytes: &mut [u8]) -> Result<usize> {
942 if self.bytes.is_empty() {
943 return Err(self.terminal.take().unwrap_or_else(|| {
944 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "script exhausted")
945 .into()
946 }));
947 }
948 let count = bytes.len().min(self.bytes.len());
949 for byte in &mut bytes[..count] {
950 *byte = self.bytes.pop_front().expect("length checked");
951 }
952 Ok(count)
953 }
954
955 async fn read_exact(&mut self, bytes: &mut [u8]) -> Result<()> {
956 for byte in bytes {
957 let Some(next) = self.bytes.pop_front() else {
958 return Err(self.terminal.take().unwrap_or_else(|| {
959 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "script exhausted")
960 .into()
961 }));
962 };
963 *byte = next;
964 }
965 Ok(())
966 }
967
968 fn cancel(&mut self) {}
969 }
970
971 fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
972 mutex
973 .lock()
974 .unwrap_or_else(std::sync::PoisonError::into_inner)
975 }
976
977 fn control_frame(message: &ControlMessage) -> VecDeque<u8> {
978 let payload = postcard::to_allocvec(message).unwrap();
979 let mut frame = VecDeque::from(u32::try_from(payload.len()).unwrap().to_be_bytes());
980 frame.extend(payload);
981 frame
982 }
983
984 fn probe_bytes(magic: [u8; 4], test_id: u64, sequence: u64, kind: ProbeKind) -> Vec<u8> {
985 postcard::to_allocvec(&Probe {
986 magic,
987 test_id,
988 sequence,
989 kind,
990 })
991 .unwrap()
992 }
993
994 #[tokio::test]
995 async fn receiver_deadline_stop_is_a_clean_download_terminal() {
996 let result = send_download(
997 vec![Box::new(PeerStoppedSend)],
998 Duration::from_millis(1),
999 1024,
1000 )
1001 .await
1002 .unwrap();
1003 assert_eq!(result.0, 0);
1004 }
1005
1006 #[test]
1007 fn probe_budgets_match_the_declared_cadence() {
1008 assert_eq!(latency_probe_budget(2_000, 100).unwrap(), 20);
1009 assert_eq!(loss_probe_budget(2_000, 100).unwrap(), 200);
1010 assert!(latency_probe_budget(2_000, 0).is_err());
1011 assert!(loss_probe_budget(2_000, MAX_PROBE_RATE_PER_SECOND + 1).is_err());
1012 }
1013
1014 #[tokio::test]
1015 async fn probe_echo_is_scoped_deduplicated_and_bounded() {
1016 let session = Arc::new(ProbeSession {
1017 datagrams: Mutex::new(VecDeque::from([
1018 probe_bytes(*b"NOPE", 42, 0, ProbeKind::Request),
1019 probe_bytes(PROBE_MAGIC, 42, 0, ProbeKind::Request),
1020 probe_bytes(PROBE_MAGIC, 42, 0, ProbeKind::Request),
1021 probe_bytes(PROBE_MAGIC, 7, 1, ProbeKind::Request),
1022 probe_bytes(PROBE_MAGIC, 42, 1, ProbeKind::Response),
1023 probe_bytes(PROBE_MAGIC, 42, 1, ProbeKind::Request),
1024 ])),
1025 sent: Mutex::new(Vec::new()),
1026 });
1027
1028 echo_datagrams(
1029 Arc::clone(&session) as Arc<dyn NetBenchSession>,
1030 42,
1031 Duration::from_secs(1),
1032 2,
1033 )
1034 .await
1035 .unwrap();
1036
1037 let sent = lock_unpoisoned(&session.sent);
1038 assert_eq!(sent.len(), 2);
1039 for (sequence, bytes) in sent.iter().enumerate() {
1040 let probe: Probe = postcard::from_bytes(bytes).unwrap();
1041 assert_eq!(probe.magic, PROBE_MAGIC);
1042 assert_eq!(probe.test_id, 42);
1043 assert_eq!(probe.sequence, sequence as u64);
1044 assert_eq!(probe.kind, ProbeKind::Response);
1045 }
1046 }
1047
1048 #[tokio::test]
1049 async fn responder_propagates_control_session_errors() {
1050 let hello = ControlMessage::ClientHello {
1051 protocol_versions: vec![PROTOCOL_VERSION],
1052 capabilities: Capabilities::default(),
1053 };
1054 let flow = NetBenchFlow::new(
1055 Arc::new(ProbeSession::default()),
1056 Box::new(SinkSend),
1057 Box::new(ScriptedReceive {
1058 bytes: control_frame(&hello),
1059 terminal: Some(Error::Network("dispatcher closed".to_owned())),
1060 }),
1061 );
1062
1063 let error = NetBenchResponder::default().serve(flow).await.unwrap_err();
1064 assert!(matches!(
1065 error,
1066 Error::Network(message) if message == "dispatcher closed"
1067 ));
1068 }
1069}