Skip to main content

irtt_client/
event.rs

1use std::{
2    net::SocketAddr,
3    time::{Duration, Instant},
4};
5
6use crate::{session::NegotiatedParams, timing::ClientTimestamp};
7
8/// Result of the IRTT open exchange.
9///
10/// The lower-level [`Client`](crate::Client) API returns this before any echo
11/// probes are driven. Managed sessions publish the contained lifecycle event to
12/// subscribers immediately after a successful open.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum OpenOutcome {
15    /// A normal probe session was opened.
16    Started {
17        /// Resolved remote socket address used by the UDP socket.
18        remote: SocketAddr,
19        /// Session token assigned by the server and used on echo/close packets.
20        token: u64,
21        /// Parameters returned by the server after negotiation.
22        negotiated: NegotiatedParams,
23        /// Lifecycle event corresponding to this outcome.
24        event: ClientEvent,
25    },
26    /// A no-test open exchange completed without starting a probe session.
27    NoTestCompleted {
28        /// Resolved remote socket address used by the UDP socket.
29        remote: SocketAddr,
30        /// Parameters returned by the server after negotiation.
31        negotiated: NegotiatedParams,
32        /// Lifecycle event corresponding to this outcome.
33        event: ClientEvent,
34    },
35}
36
37/// Event emitted by an IRTT client session.
38///
39/// Events form the public lifecycle and measurement stream for callers. UIs
40/// usually display lifecycle, warning, and per-packet events directly; summary
41/// reporting should aggregate `EchoSent`, `EchoReply`, `EchoLoss`,
42/// `LateReply`, and `DuplicateReply` events or pass the stream to
43/// `irtt-stats`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ClientEvent {
46    /// A normal session has opened and is ready to send echo probes.
47    SessionStarted {
48        /// Resolved remote socket address.
49        remote: SocketAddr,
50        /// Session token assigned by the server.
51        token: u64,
52        /// Negotiated protocol parameters for this session.
53        negotiated: NegotiatedParams,
54        /// Client timestamp when the open reply was accepted.
55        at: ClientTimestamp,
56    },
57    /// A negotiation-only no-test exchange completed.
58    ///
59    /// No `EchoSent`, `EchoReply`, `EchoLoss`, or `SessionClosed` events are
60    /// expected for this open outcome.
61    NoTestCompleted {
62        /// Resolved remote socket address.
63        remote: SocketAddr,
64        /// Negotiated protocol parameters returned by the server.
65        negotiated: NegotiatedParams,
66        /// Client timestamp when the open reply was accepted.
67        at: ClientTimestamp,
68    },
69    /// The client sent a close request and considers the session closed.
70    ///
71    /// This is a local lifecycle event, not an acknowledgement from the server.
72    /// Managed sessions disconnect event subscribers after the worker exits,
73    /// while leaving already queued events available to drain.
74    SessionClosed {
75        /// Resolved remote socket address.
76        remote: SocketAddr,
77        /// Session token that was closed.
78        token: u64,
79        /// Client timestamp when the close request was sent.
80        at: ClientTimestamp,
81    },
82
83    /// An echo request was sent.
84    ///
85    /// This records local scheduling and send-call timing. It does not imply a
86    /// reply will arrive; callers should pair it with `EchoReply`, `EchoLoss`,
87    /// `LateReply`, or `DuplicateReply` by sequence number when displaying
88    /// packet lifecycle state.
89    EchoSent {
90        /// Wire sequence number sent in the echo request.
91        seq: u32,
92        /// Resolved remote socket address.
93        remote: SocketAddr,
94        /// Monotonic deadline at which the probe was scheduled to be sent.
95        scheduled_at: Instant,
96        /// Client wall/monotonic timestamp captured for the send.
97        sent_at: ClientTimestamp,
98        /// Number of bytes passed to the UDP socket.
99        bytes: usize,
100        /// Elapsed time spent in the socket send call.
101        send_call: Duration,
102        /// Absolute difference between the scheduled send time and actual send
103        /// timestamp.
104        timer_error: Duration,
105    },
106
107    /// First in-window reply for a pending echo request.
108    ///
109    /// This is the primary successful measurement event. RTT and optional
110    /// server timing/statistics fields should generally be aggregated from this
111    /// variant.
112    EchoReply {
113        /// Wire sequence number from the echo reply.
114        seq: u32,
115        /// Resolved remote socket address.
116        remote: SocketAddr,
117        /// Client timestamp recorded when the matching request was sent.
118        sent_at: ClientTimestamp,
119        /// Client timestamp recorded when the reply was received.
120        received_at: ClientTimestamp,
121        /// Round-trip timing sample for this reply.
122        rtt: RttSample,
123        /// Optional server-reported timestamps and processing time.
124        server_timing: Option<ServerTiming>,
125        /// Optional one-way delay values derived from client/server wall clocks.
126        one_way: Option<OneWayDelaySample>,
127        /// Optional server-reported received-statistics sample.
128        received_stats: Option<ReceivedStatsSample>,
129        /// UDP datagram size received from the socket.
130        bytes: usize,
131        /// Receive-side metadata observed outside the IRTT wire payload.
132        packet_meta: PacketMeta,
133    },
134
135    /// A sent echo request exceeded the local probe timeout before a reply was
136    /// matched.
137    ///
138    /// A later datagram for the same sequence may still arrive and will be
139    /// reported as [`ClientEvent::LateReply`] rather than changing this event.
140    EchoLoss {
141        /// Wire sequence number that timed out.
142        seq: u32,
143        /// Client timestamp recorded when the request was sent.
144        sent_at: ClientTimestamp,
145        /// Monotonic deadline at which the probe was declared lost.
146        timeout_at: std::time::Instant,
147    },
148
149    /// Additional reply for a sequence that was already completed.
150    ///
151    /// Duplicates are not primary RTT samples. They are useful for diagnostics
152    /// and packet counters, but should not be aggregated as successful first
153    /// replies.
154    DuplicateReply {
155        /// Wire sequence number from the duplicate reply.
156        seq: u32,
157        /// Resolved remote socket address.
158        remote: SocketAddr,
159        /// Client timestamp recorded when the duplicate was received.
160        received_at: ClientTimestamp,
161        /// UDP datagram size received from the socket.
162        bytes: usize,
163    },
164
165    /// Reply for a sequence that was no longer pending.
166    ///
167    /// This commonly means the probe had already been reported as lost, or the
168    /// reply arrived out of the client's current tracking window. When the
169    /// original send metadata is still retained, RTT and one-way timing are
170    /// included; otherwise those fields are `None`.
171    LateReply {
172        /// Wire sequence number from the late reply.
173        seq: u32,
174        /// Highest sequence number accepted as an in-window first reply so far.
175        highest_seen: u32,
176        /// Resolved remote socket address.
177        remote: SocketAddr,
178        /// Original client send timestamp, when still retained.
179        sent_at: Option<ClientTimestamp>,
180        /// Client timestamp recorded when the late reply was received.
181        received_at: ClientTimestamp,
182        /// RTT sample computed when the original send timestamp is available.
183        rtt: Option<RttSample>,
184        /// Optional server-reported timestamps and processing time.
185        server_timing: Option<ServerTiming>,
186        /// Optional one-way delay values derived from client/server wall clocks.
187        one_way: Option<OneWayDelaySample>,
188        /// Optional server-reported received-statistics sample.
189        received_stats: Option<ReceivedStatsSample>,
190        /// UDP datagram size received from the socket.
191        bytes: usize,
192        /// Receive-side metadata observed outside the IRTT wire payload.
193        packet_meta: PacketMeta,
194    },
195
196    /// Non-fatal condition observed while receiving or classifying packets.
197    ///
198    /// Warning events are diagnostic. They do not close the session by
199    /// themselves and are not measurement samples.
200    Warning {
201        /// Machine-readable warning category.
202        kind: WarningKind,
203        /// Human-readable diagnostic text.
204        message: String,
205        /// Client timestamp when the warning was emitted.
206        at: ClientTimestamp,
207    },
208}
209
210/// Category for a non-fatal [`ClientEvent::Warning`].
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212#[non_exhaustive]
213pub enum WarningKind {
214    /// A received datagram could not be decoded as a relevant IRTT packet.
215    MalformedOrUnrelatedPacket,
216    /// A decoded packet used a token that does not match the open session.
217    WrongToken,
218    /// A decoded reply could not be matched to tracked probe state.
219    UntrackedReply,
220}
221
222/// Round-trip timing for one echo reply.
223///
224/// `raw` is the client-observed elapsed time from sending the echo request to
225/// receiving the reply and is therefore non-negative. When the server reports
226/// enough monotonic timing to compute processing time, `adjusted` subtracts
227/// that server processing from `raw`. `effective` is the value used by reporting
228/// and statistics: adjusted when available, otherwise raw converted to a signed
229/// value.
230///
231/// Adjusted and effective RTT values are signed. They may be negative if the
232/// reported server processing time is greater than the raw client-observed RTT.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct RttSample {
235    /// Client-observed RTT from send to receive.
236    pub raw: Duration,
237    /// Signed RTT adjusted for server processing when server processing is available.
238    pub adjusted: Option<SignedDuration>,
239    /// Signed effective RTT used by stats and CLI reporting.
240    ///
241    /// This is `adjusted` when server processing is available, otherwise `raw`
242    /// converted to a signed duration.
243    pub effective: SignedDuration,
244}
245
246/// Signed nanosecond-backed duration used for derived timing values.
247///
248/// This type preserves negative values that can arise after subtracting server
249/// processing time or comparing wall-clock timestamps from different hosts. It
250/// should not be used to represent raw elapsed wall-clock or monotonic time,
251/// which is non-negative and represented by [`Duration`].
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
253pub struct SignedDuration {
254    ns: i128,
255}
256
257impl SignedDuration {
258    /// Construct a signed duration from nanoseconds.
259    pub const fn from_nanos(ns: i128) -> Self {
260        Self { ns }
261    }
262
263    /// Convert a non-negative [`Duration`] into a signed duration.
264    ///
265    /// Durations larger than `i128::MAX` nanoseconds saturate to `i128::MAX`.
266    pub fn from_duration(duration: Duration) -> Self {
267        Self {
268            ns: i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX),
269        }
270    }
271
272    /// Return the duration in nanoseconds.
273    pub const fn as_nanos(self) -> i128 {
274        self.ns
275    }
276
277    /// Return the duration in whole microseconds, truncated toward zero.
278    pub const fn as_micros(self) -> i128 {
279        self.ns / 1_000
280    }
281
282    /// Return the duration in whole milliseconds, truncated toward zero.
283    pub const fn as_millis(self) -> i128 {
284        self.ns / 1_000_000
285    }
286
287    /// Return whether this duration is less than zero.
288    pub const fn is_negative(self) -> bool {
289        self.ns < 0
290    }
291}
292
293impl From<Duration> for SignedDuration {
294    fn from(value: Duration) -> Self {
295        Self::from_duration(value)
296    }
297}
298
299/// Server-reported timing fields decoded from an echo reply.
300///
301/// These values are optional because the negotiated timestamp mode, clock mode,
302/// server implementation, or packet contents may omit individual fields.
303/// Timestamp values are nanoseconds in the server's reported wall-clock or
304/// monotonic clock domain. `None` means unavailable; `Some(0)` means the server
305/// reported a zero timestamp.
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub struct ServerTiming {
308    /// Server wall-clock receive timestamp, in nanoseconds.
309    pub receive_wall_ns: Option<i64>,
310    /// Server monotonic receive timestamp, in nanoseconds.
311    pub receive_mono_ns: Option<i64>,
312    /// Server wall-clock send timestamp, in nanoseconds.
313    pub send_wall_ns: Option<i64>,
314    /// Server monotonic send timestamp, in nanoseconds.
315    pub send_mono_ns: Option<i64>,
316    /// Server wall-clock midpoint timestamp, in nanoseconds.
317    pub midpoint_wall_ns: Option<i64>,
318    /// Server monotonic midpoint timestamp, in nanoseconds.
319    pub midpoint_mono_ns: Option<i64>,
320    /// Server processing time computed from monotonic receive/send timestamps.
321    ///
322    /// `None` means the required monotonic timestamps were unavailable or
323    /// unusable. `Some(Duration::ZERO)` means the computed processing interval
324    /// was available and zero.
325    pub processing: Option<Duration>,
326}
327
328/// Directional one-way delay samples computed from wall-clock timestamps.
329///
330/// Each directional field is an `Option<SignedDuration>`. `None` means the
331/// required client or server wall-clock timestamp was unavailable, or the
332/// timestamp arithmetic was out of range. `Some(value)` means the delay was
333/// computed from available timestamps.
334///
335/// Negative computed values are preserved as `Some` instead of being clamped to
336/// zero or dropped. A negative one-way delay usually indicates wall-clock skew
337/// between the client and server.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub struct OneWayDelaySample {
340    /// Client-to-server delay.
341    ///
342    /// This is computed from the client send wall time and the server receive
343    /// wall time when both are available.
344    pub client_to_server: Option<SignedDuration>,
345    /// Server-to-client delay.
346    ///
347    /// This is computed from the server send wall time and the client receive
348    /// wall time when both are available.
349    pub server_to_client: Option<SignedDuration>,
350}
351
352/// Server-reported receive counters decoded from an echo reply.
353///
354/// These fields are optional because the negotiated received-statistics mode or
355/// server implementation may omit them. `None` means unavailable; `Some(0)`
356/// means the server reported the value and it was zero.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct ReceivedStatsSample {
359    /// Server-reported count of received packets, when available.
360    pub count: Option<u32>,
361    /// Server-reported receive window value, when available.
362    pub window: Option<u64>,
363}
364
365/// Receive-side packet metadata observed outside the IRTT wire protocol.
366///
367/// `None` means the metadata was unavailable. `Some(0)` means the metadata was
368/// observed and its value was zero.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
370pub struct PacketMeta {
371    pub traffic_class: Option<u8>,
372    pub dscp: Option<u8>,
373    pub ecn: Option<u8>,
374    pub kernel_rx_timestamp: Option<std::time::SystemTime>,
375}