Skip to main content

tokio_client/
tokio_client.rs

1//! Low-level Tokio client example (`tokio` feature).
2//!
3//! Drives [`AsyncClient`] directly on a caller-owned runtime. This is the
4//! adapter to reach for when you already have a Tokio runtime and want to
5//! await sends/receives yourself, rather than use the higher-level `managed`
6//! driver (see the `managed_client` example).
7//!
8//! Run a server first, then run this example:
9//!
10//! ```text
11//! cargo run -p irtt-rs --bin irtt-server --features server
12//! cargo run -p irtt-client --example tokio_client --features tokio
13//! ```
14//!
15//! Without a reachable server this exits quickly with an open-timeout error
16//! instead of hanging, because the example shortens `open.timeouts` for a
17//! fast demonstration.
18
19use std::time::{Duration, Instant};
20
21use irtt_client::{
22    AsyncClient, ClientConfig, ClientEvent, OpenOutcome, OpenPolicy, SessionRequest,
23};
24
25/// Upper bound on how long the receive wait can go without also calling
26/// `poll_timeouts()`. `AsyncClient` does not expose the earliest pending
27/// probe's own timeout deadline, so this example polls on a short fixed
28/// cadence instead of waiting for the full `probe_timeout()` from whenever
29/// this loop last ran — that would let poll_timeouts() classify a lost
30/// probe (and its dependent statistics/session state) far later than it
31/// actually became lost.
32const TIMEOUT_POLL_INTERVAL: Duration = Duration::from_millis(50);
33
34fn main() {
35    let runtime = tokio::runtime::Builder::new_current_thread()
36        .enable_io()
37        .enable_time()
38        .build()
39        .expect("failed to build Tokio runtime");
40
41    runtime.block_on(run());
42}
43
44async fn run() {
45    let config = ClientConfig {
46        open: OpenPolicy {
47            // A single short attempt keeps this example fast when no server is
48            // reachable; production callers should generally keep the default.
49            timeouts: vec![Duration::from_millis(300)],
50            ..Default::default()
51        },
52        request: SessionRequest {
53            duration: Some(Duration::from_secs(2)),
54            interval: Duration::from_millis(200),
55            ..Default::default()
56        },
57        ..Default::default()
58    };
59
60    let mut client = match AsyncClient::connect("127.0.0.1:2112", config).await {
61        Ok(client) => client,
62        Err(err) => {
63            eprintln!("failed to prepare client socket: {err}");
64            return;
65        }
66    };
67
68    let negotiated = match client.open().await {
69        Ok(OpenOutcome::Started(started)) => {
70            println!("session opened: {started:?}");
71            started.negotiation
72        }
73        Ok(OpenOutcome::NoTestCompleted(completed)) => {
74            println!("{completed:?}");
75            return;
76        }
77        Err(err) => {
78            eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
79            return;
80        }
81    };
82
83    // This loop owns cadence and duration. The transport sends whenever called.
84    let interval = negotiated.accepted.interval;
85    let start = Instant::now();
86    let end = start
87        + negotiated
88            .accepted
89            .duration
90            .expect("example requests a finite run");
91    let deadline = end + client.probe_timeout();
92    let mut next_send = start;
93    while Instant::now() < deadline && !client.is_peer_closed() {
94        if Instant::now() >= end && !client.has_pending_probes() {
95            break;
96        }
97        // Bound the receive wait by the next send/timeout deadline, rather
98        // than awaiting recv() unconditionally: a lost or rate-limited reply
99        // must not stall pacing or timeout classification indefinitely.
100        let wake_at = (next_send < end)
101            .then_some(next_send)
102            .into_iter()
103            .chain(client.next_probe_timeout_deadline())
104            .chain(std::iter::once(Instant::now() + TIMEOUT_POLL_INTERVAL))
105            .min()
106            .unwrap();
107
108        let recv_result = tokio::select! {
109            events = client.recv() => Some(events),
110            () = tokio::time::sleep_until(tokio::time::Instant::from_std(wake_at)) => None,
111        };
112        if let Some(result) = recv_result {
113            match result {
114                Ok(events) => events.iter().for_each(print_event),
115                Err(err) => {
116                    eprintln!("receive failed: {err}");
117                    break;
118                }
119            }
120        }
121
122        if Instant::now() < end && Instant::now() >= next_send {
123            match client.send_probe().await {
124                Ok(receipt) => {
125                    print_event(&receipt.into());
126                    while next_send <= Instant::now() {
127                        next_send += interval;
128                    }
129                }
130                Err(err) => {
131                    if let irtt_client::SendProbeError::AfterCommit { receipt, .. } = &err {
132                        print_event(&(*receipt).into());
133                    }
134                    eprintln!("send failed: {err}");
135                    break;
136                }
137            }
138        }
139        match client.poll_timeouts() {
140            Ok(events) => events.iter().for_each(print_event),
141            Err(err) => {
142                eprintln!("timeout polling failed: {err}");
143                break;
144            }
145        }
146    }
147
148    if client.is_peer_closed() {
149        return;
150    }
151    match client.close().await {
152        Ok(events) => events.iter().for_each(print_event),
153        Err(err) => eprintln!("close failed: {err}"),
154    }
155}
156
157fn print_event(event: &ClientEvent) {
158    match event {
159        ClientEvent::EchoReply { seq, rtt, .. } => {
160            println!("seq {seq}: rtt {:?}", rtt.effective);
161        }
162        ClientEvent::EchoLoss { seq, .. } => println!("seq {seq}: lost"),
163        other => println!("{other:?}"),
164    }
165}