irtt-client 0.6.0

IRTT-compatible Rust client library
Documentation
use std::{collections::TryReserveError, 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 {
    /// A Tokio-backed client operation was polled without a current runtime.
    #[cfg(feature = "tokio")]
    #[error("Tokio AsyncClient requires a current Tokio runtime")]
    NoTokioRuntime,
    /// 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 prepared session was not committed, and the client remains
    /// connected rather than partially open.
    #[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 },
    /// A wrapping wire sequence is still represented by an in-flight probe.
    ///
    /// The existing probe must be replied to or timed out before that sequence
    /// can be reused.
    #[error("wire sequence {seq} is still pending")]
    PendingSequenceCollision { seq: u32 },
    /// A prepared probe no longer matches the session's next wire sequence.
    ///
    /// Prepare the probe again after any intervening successful send.
    #[error(
        "prepared probe sequence {prepared_seq} is stale; next wire sequence is {next_wire_seq}"
    )]
    StalePreparedProbe {
        prepared_seq: u32,
        next_wire_seq: u32,
    },
    /// Reserving memory required by an operation failed before any datagram was
    /// sent or logical protocol state was changed.
    #[error("failed to reserve memory for {operation}: {source}")]
    AllocationFailed {
        operation: &'static str,
        #[source]
        source: TryReserveError,
    },
    /// UDP reported successful acceptance of a datagram with an unexpected
    /// byte count.
    ///
    /// The send is committed locally before this transport invariant failure
    /// is returned because the kernel reported success.
    #[error("UDP accepted {actual} bytes for a {expected}-byte datagram")]
    DatagramLengthMismatch { expected: usize, actual: 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 },
}