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}