docs: document client errors and bounds

This commit is contained in:
Nils Andreas Svee committed 2026-05-18 10:56:58 +07:00
1 parent 01c10e2e0d
commit 6f77d38a7f
2 files changed
+131

No files matched your search

+21
View File
@@ -1,11 +1,32 @@
use std::{net::SocketAddr, time::Duration}; 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; pub use irtt_proto::MAX_SERVER_FILL_BYTES;
use irtt_proto::{Clock, ReceivedStats, StampAt}; use irtt_proto::{Clock, ReceivedStats, StampAt};
pub(crate) const DEFAULT_PORT: u16 = 2112; 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; 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; 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 const MAX_UDP_PAYLOAD_LENGTH: u32 = 65_507;
pub(crate) const DEFAULT_DURATION: Duration = Duration::from_secs(3); pub(crate) const DEFAULT_DURATION: Duration = Duration::from_secs(3);
pub(crate) const DEFAULT_INTERVAL: Duration = Duration::from_secs(1); pub(crate) const DEFAULT_INTERVAL: Duration = Duration::from_secs(1);
+110
View File
@@ -2,66 +2,176 @@ use std::{io, time::Duration};
use thiserror::Error; 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)] #[derive(Debug, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum ClientError { 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:?}")] #[error("failed to resolve server address {addr:?}")]
Resolve { addr: String }, 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}")] #[error("socket error: {0}")]
Socket(#[from] io::Error), 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}")] #[error("socket option error while trying to {operation} for {remote}: {source}")]
SocketOption { SocketOption {
operation: &'static str, operation: &'static str,
remote: std::net::SocketAddr, remote: std::net::SocketAddr,
source: io::Error, 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}")] #[error("failed to restore configured socket read timeout after open negotiation: {source}")]
ReadTimeoutRestore { source: io::Error }, 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}")] #[error("protocol error: {0}")]
Protocol(#[from] irtt_proto::ProtoError), 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")] #[error("all open requests timed out")]
OpenTimeout, 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:?}")] #[error("open timeout {timeout:?} is below the minimum {minimum:?}")]
OpenTimeoutTooSmall { OpenTimeoutTooSmall {
timeout: Duration, timeout: Duration,
minimum: 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")] #[error("open_timeouts must not be empty")]
NoOpenTimeouts, 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")] #[error("server rejected the open request")]
ServerRejected, 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")] #[error("unexpected no-test open reply")]
UnexpectedNoTestReply, 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}")] #[error("no-test reply included a non-zero connection token: {token}")]
NonZeroNoTestToken { token: u64 }, 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}")] #[error("protocol version mismatch: requested {requested}, received {received}")]
ProtocolVersionMismatch { requested: i64, received: i64 }, 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")] #[error("server returned a zero connection token")]
ZeroToken, 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}")] #[error("strict negotiation rejected changed params: {reason}")]
NegotiationRejected { reason: String }, NegotiationRejected { reason: String },
/// The requested operation requires an open session, but the client is only
/// connected.
#[error("client is not open")] #[error("client is not open")]
NotOpen, 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")] #[error("client session is already open")]
AlreadyOpen, AlreadyOpen,
/// The no-test session has already completed.
///
/// Create a new client session to perform another negotiation.
#[error("client session is already completed")] #[error("client session is already completed")]
AlreadyCompleted, AlreadyCompleted,
/// The session is already closed.
///
/// Create a new client session before opening or running another test.
#[error("client session is already closed")] #[error("client session is already closed")]
AlreadyClosed, 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")] #[error("duration is too large to encode as nanoseconds")]
DurationOverflow, 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")] #[error("counter {counter} overflowed")]
CounterOverflow { counter: &'static str }, 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})")] #[error("pending probe limit exceeded ({limit})")]
PendingLimitExceeded { limit: usize }, 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}")] #[error("invalid configuration: {reason}")]
InvalidConfig { reason: String }, 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")] #[error("managed client worker thread panicked")]
WorkerPanicked, 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)] #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub enum EventSubscriptionError { 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")] #[error("event subscription is disconnected")]
Disconnected, Disconnected,
} }