Each published crate (irtt-rs, irtt-client, irtt-server, irtt-stats, irtt-proto) now points crates.io at its own README instead of inheriting the root project README via the workspace-wide readme default, which is removed. Each crate README is a short landing page: what the crate is, who should use it, which sibling crate to use instead if this isn't the right abstraction level, and links to docs.rs and the main repository. Add real Cargo example targets teaching irtt-client's blocking/Tokio/ managed API tiers, a basic and a multi-listener irtt-server example, and an irtt-stats example contrasting finite vs continuous retention. irtt-proto intentionally has none. Update each crate's top-level rustdoc to explain its API-tier choices and point at the examples, and enable docs.rs rustdoc example scraping for irtt-client and irtt-server (via doc-scrape-examples on each example target, needed because the crate has dev-dependencies). The client examples are caller-paced against next_send_deadline() rather than sending on every loop iteration, bound their receive wait by the next send/timeout deadline instead of awaiting indefinitely, poll timeouts on a short fixed cadence, and surface send/receive/close errors instead of discarding them via unwrap_or_default()/if-let-Ok -- so they demonstrate correct pacing and terminate/report cleanly rather than racing the server, hanging on a lost reply, or silently dropping a failed final close.
134 lines
4.7 KiB
Rust
134 lines
4.7 KiB
Rust
//! Low-level Tokio client example (`tokio` feature).
|
|
//!
|
|
//! Drives [`AsyncClient`] directly on a caller-owned runtime. This is the
|
|
//! adapter to reach for when you already have a Tokio runtime and want to
|
|
//! await sends/receives yourself, rather than use the higher-level `managed`
|
|
//! driver (see the `managed_client` example).
|
|
//!
|
|
//! Run a server first, then run this example:
|
|
//!
|
|
//! ```text
|
|
//! cargo run -p irtt-rs --bin irtt-server --features server
|
|
//! cargo run -p irtt-client --example tokio_client --features tokio
|
|
//! ```
|
|
//!
|
|
//! Without a reachable server this exits quickly with an open-timeout error
|
|
//! instead of hanging, because the example shortens `open_timeouts` for a
|
|
//! fast demonstration.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use irtt_client::{AsyncClient, ClientConfig, ClientEvent};
|
|
|
|
/// Upper bound on how long the receive wait can go without also calling
|
|
/// `poll_timeouts()`. `AsyncClient` does not expose the earliest pending
|
|
/// probe's own timeout deadline, so this example polls on a short fixed
|
|
/// cadence instead of waiting for the full `probe_timeout()` from whenever
|
|
/// this loop last ran — that would let poll_timeouts() classify a lost
|
|
/// probe (and its dependent statistics/session state) far later than it
|
|
/// actually became lost.
|
|
const TIMEOUT_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
|
|
|
fn main() {
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_io()
|
|
.enable_time()
|
|
.build()
|
|
.expect("failed to build Tokio runtime");
|
|
|
|
runtime.block_on(run());
|
|
}
|
|
|
|
async fn run() {
|
|
let config = ClientConfig {
|
|
server_addr: "127.0.0.1:2112".to_owned(),
|
|
duration: Some(Duration::from_secs(2)),
|
|
interval: Duration::from_millis(200),
|
|
// A single short attempt keeps this example fast when no server is
|
|
// reachable; production callers should generally keep the default.
|
|
open_timeouts: vec![Duration::from_millis(300)],
|
|
..Default::default()
|
|
};
|
|
|
|
let mut client = match AsyncClient::connect(config).await {
|
|
Ok(client) => client,
|
|
Err(err) => {
|
|
eprintln!("failed to prepare client socket: {err}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
match client.open().await {
|
|
Ok(outcome) => println!("session opened: {outcome:?}"),
|
|
Err(err) => {
|
|
eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
while !client.is_run_complete() {
|
|
// Bound the receive wait by the next send/timeout deadline, rather
|
|
// than awaiting recv() unconditionally: a lost or rate-limited reply
|
|
// must not stall pacing or timeout classification indefinitely.
|
|
let wake_at = client
|
|
.next_send_deadline()
|
|
.into_iter()
|
|
.chain(std::iter::once(Instant::now() + TIMEOUT_POLL_INTERVAL))
|
|
.min()
|
|
.expect("the fixed poll-interval deadline is always present");
|
|
|
|
let recv_result = tokio::select! {
|
|
events = client.recv() => Some(events),
|
|
() = tokio::time::sleep_until(tokio::time::Instant::from_std(wake_at)) => None,
|
|
};
|
|
if let Some(result) = recv_result {
|
|
match result {
|
|
Ok(events) => events.iter().for_each(print_event),
|
|
Err(err) => {
|
|
eprintln!("receive failed: {err}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if client
|
|
.next_send_deadline()
|
|
.is_some_and(|send_at| Instant::now() >= send_at)
|
|
{
|
|
// A failed send retains its prepared probe without advancing the
|
|
// schedule, so a persistent error (e.g. the interface going
|
|
// down) would otherwise make this loop retry forever instead of
|
|
// reporting it.
|
|
match client.send_probe().await {
|
|
Ok(events) => events.iter().for_each(print_event),
|
|
Err(err) => {
|
|
eprintln!("send failed: {err}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
match client.poll_timeouts() {
|
|
Ok(events) => events.iter().for_each(print_event),
|
|
Err(err) => {
|
|
eprintln!("timeout polling failed: {err}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
match client.close().await {
|
|
Ok(events) => events.iter().for_each(print_event),
|
|
Err(err) => eprintln!("close failed: {err}"),
|
|
}
|
|
}
|
|
|
|
fn print_event(event: &ClientEvent) {
|
|
match event {
|
|
ClientEvent::EchoReply { seq, rtt, .. } => {
|
|
println!("seq {seq}: rtt {:?}", rtt.effective);
|
|
}
|
|
ClientEvent::EchoLoss { seq, .. } => println!("seq {seq}: lost"),
|
|
other => println!("{other:?}"),
|
|
}
|
|
}
|