From 6f77d38a7f1676825a158bb39f197a9c9157faa4 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Mon, 18 May 2026 10:56:58 +0700 Subject: [PATCH] docs: document client errors and bounds --- crates/irtt-client/src/config.rs | 21 ++++++ crates/irtt-client/src/error.rs | 110 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/crates/irtt-client/src/config.rs b/crates/irtt-client/src/config.rs index 93bdc5c..c0b1817 100644 --- a/crates/irtt-client/src/config.rs +++ b/crates/irtt-client/src/config.rs @@ -1,11 +1,32 @@ use std::{net::SocketAddr, time::Duration}; +/// Protocol compatibility bound for a requested `server_fill` value, in UTF-8 +/// bytes. +/// +/// This is the maximum encoded server-fill string accepted by this client and +/// protocol decoder. [`ClientConfig::server_fill`] enforces the same bound +/// before opening a session. +pub use irtt_proto::MAX_SERVER_FILL_BYTES; pub use irtt_proto::MAX_SERVER_FILL_BYTES; use irtt_proto::{Clock, ReceivedStats, StampAt}; pub(crate) const DEFAULT_PORT: u16 = 2112; +/// Largest valid DSCP codepoint accepted by client configuration. +/// +/// DSCP is a six-bit value in the range `0..=63`. The client maps it into the +/// socket traffic-class field when configuring packet priority. pub const MAX_DSCP_CODEPOINT: u8 = 63; +/// Largest IPv4 TTL or IPv6 hop-limit value accepted by client configuration. +/// +/// This is the public user-configuration bound for +/// [`SocketConfig::ttl`]. A value of zero is rejected separately because socket +/// TTL and hop-limit settings are configured as `1..=255`. pub const MAX_TTL: u32 = 255; +/// Largest UDP payload length accepted by client configuration, in bytes. +/// +/// This is the maximum UDP payload size excluding IP and UDP headers. It caps +/// [`ClientConfig::length`] before protocol packets are encoded or sent. +pub const MAX_UDP_PAYLOAD_LENGTH: u32 = 65_507; pub const MAX_UDP_PAYLOAD_LENGTH: u32 = 65_507; pub(crate) const DEFAULT_DURATION: Duration = Duration::from_secs(3); pub(crate) const DEFAULT_INTERVAL: Duration = Duration::from_secs(1); diff --git a/crates/irtt-client/src/error.rs b/crates/irtt-client/src/error.rs index d6fbe1a..164d247 100644 --- a/crates/irtt-client/src/error.rs +++ b/crates/irtt-client/src/error.rs @@ -2,66 +2,176 @@ use std::{io, time::Duration}; use thiserror::Error; +/// Failure reported while configuring, opening, running, receiving, closing, or +/// joining an IRTT client session. +/// +/// Configuration validation errors usually require changing +/// [`ClientConfig`](crate::ClientConfig) or related options before retrying. +/// Network, protocol, and runtime errors may be transient, but can also reflect +/// peer incompatibility or an invalid session state. #[derive(Debug, Error)] #[non_exhaustive] pub enum ClientError { + /// The configured server address could not be resolved to a socket address. + /// + /// Check the host, port, address-family restrictions, and name service + /// configuration before retrying. #[error("failed to resolve server address {addr:?}")] Resolve { addr: String }, + /// A UDP socket operation failed while binding, connecting, sending, or + /// receiving. + /// + /// This can be a transient network or OS error, but may also reflect local + /// permissions, routing, or socket configuration. #[error("socket error: {0}")] Socket(#[from] io::Error), + /// Applying or reading a socket option failed. + /// + /// This is usually caused by an unsupported option, an invalid local value, + /// or a platform-specific socket limitation. Changing socket configuration + /// is more likely to help than retrying unchanged. #[error("socket option error while trying to {operation} for {remote}: {source}")] SocketOption { operation: &'static str, remote: std::net::SocketAddr, source: io::Error, }, + /// Restoring the configured read timeout after open negotiation failed. + /// + /// The session may have opened successfully, but the socket could not be + /// returned to the caller's requested receive-timeout behavior. #[error("failed to restore configured socket read timeout after open negotiation: {source}")] ReadTimeoutRestore { source: io::Error }, + /// A packet failed IRTT protocol parsing or authentication. + /// + /// Likely causes include malformed input, unsupported protocol data, or an + /// HMAC mismatch between client and server. #[error("protocol error: {0}")] Protocol(#[from] irtt_proto::ProtoError), + /// No valid open reply was received before all configured open attempts + /// timed out. + /// + /// Retrying can succeed when the server or network is temporarily + /// unavailable. Persistent failures usually require changing the address, + /// authentication key, server configuration, or timeout settings. #[error("all open requests timed out")] OpenTimeout, + /// An open-attempt timeout is below the client's supported minimum. + /// + /// Increase the configured timeout before retrying. #[error("open timeout {timeout:?} is below the minimum {minimum:?}")] OpenTimeoutTooSmall { timeout: Duration, minimum: Duration, }, + /// The configured open-attempt timeout list is empty. + /// + /// Provide at least one timeout before opening the session. #[error("open_timeouts must not be empty")] NoOpenTimeouts, + /// The server explicitly rejected the open request. + /// + /// Retrying unchanged is unlikely to help unless the server configuration or + /// load has changed. #[error("server rejected the open request")] ServerRejected, + /// A no-test open request received a reply that did not have no-test close + /// semantics. + /// + /// This indicates an incompatible or unexpected peer response. #[error("unexpected no-test open reply")] UnexpectedNoTestReply, + /// A no-test open reply included a connection token. + /// + /// No-test negotiation must not establish a probe session, so a non-zero + /// token indicates an incompatible or unexpected peer response. #[error("no-test reply included a non-zero connection token: {token}")] NonZeroNoTestToken { token: u64 }, + /// The peer replied with a protocol version different from the requested + /// version. + /// + /// This is a protocol compatibility error and requires compatible client + /// and server versions. #[error("protocol version mismatch: requested {requested}, received {received}")] ProtocolVersionMismatch { requested: i64, received: i64 }, + /// The server returned a zero connection token for a normal test session. + /// + /// A normal session requires a non-zero token to correlate echo traffic, so + /// this indicates an invalid peer response. #[error("server returned a zero connection token")] ZeroToken, + /// Negotiated parameters were rejected by the configured negotiation policy. + /// + /// Use a less restrictive configuration or + /// [`NegotiationPolicy::Loose`](crate::NegotiationPolicy::Loose) when the + /// caller accepts documented server restrictions. #[error("strict negotiation rejected changed params: {reason}")] NegotiationRejected { reason: String }, + /// The requested operation requires an open session, but the client is only + /// connected. #[error("client is not open")] NotOpen, + /// The session is already open. + /// + /// Continue running, receiving, or closing the existing session instead of + /// opening it again. #[error("client session is already open")] AlreadyOpen, + /// The no-test session has already completed. + /// + /// Create a new client session to perform another negotiation. #[error("client session is already completed")] AlreadyCompleted, + /// The session is already closed. + /// + /// Create a new client session before opening or running another test. #[error("client session is already closed")] AlreadyClosed, + /// A duration could not be represented as nanoseconds for scheduling or + /// protocol encoding. + /// + /// Use smaller configured or negotiated durations. #[error("duration is too large to encode as nanoseconds")] DurationOverflow, + /// A client-maintained counter would overflow. + /// + /// This is a runtime limit rather than a network failure. Start a new + /// session if the test duration or send volume reaches this limit. #[error("counter {counter} overflowed")] CounterOverflow { counter: &'static str }, + /// Sending another probe would exceed the configured pending-probe limit. + /// + /// Increase [`ClientConfig::max_pending_probes`](crate::ClientConfig::max_pending_probes), + /// reduce send rate, or receive replies and timeouts more often before + /// sending again. #[error("pending probe limit exceeded ({limit})")] PendingLimitExceeded { limit: usize }, + /// Client configuration failed validation before the requested operation. + /// + /// The reported reason names a value that must be changed before retrying. #[error("invalid configuration: {reason}")] InvalidConfig { reason: String }, + /// A managed-session worker thread panicked before producing an outcome. + /// + /// This is a runtime failure from joining the managed session, not a + /// protocol response from the server. #[error("managed client worker thread panicked")] WorkerPanicked, } +/// Failure reported while setting up or receiving from an event subscription. +/// +/// Zero-capacity subscriber queues are rejected when subscriptions are created. +/// Once a subscription exists, this error type reports delivery failures observed +/// by [`EventSubscription`](crate::EventSubscription). #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum EventSubscriptionError { + /// The subscription can no longer receive new events. + /// + /// Already queued events are delivered first. After the queue is drained, + /// `recv` and `try_recv` return this error when the hub disconnects the + /// subscriber, the hub is shut down, or the subscription is disconnected by + /// its overflow policy. #[error("event subscription is disconnected")] Disconnected, }