Skip to main content

sipx_transport/
counters.rs

1//! What a running endpoint will say about itself.
2//!
3//! `docs/specs/sip-transport.md` §12. Counters and nothing else: no metrics library, no exposition
4//! format, no push. A [`Counters`] snapshot is read through [`crate::Handle::counters`] and what an
5//! application does with it is the application's business — a stack that picks an exposition format
6//! picks it for every user of the library, and that is the one observability decision here that
7//! cannot be undone later.
8//!
9//! The atomics live behind an `Arc` shared with every handle rather than being answered by the
10//! event loop, which is the same choice [`ShedCounts`] made for the same reason: the loop is busy in
11//! precisely the situation these numbers describe. `Handle::shed` is synchronous; the `async`
12//! `Handle::outstanding` beside it has to ask the loop and can fail because of it. A snapshot that
13//! could return `Err(EndpointClosed)` under load would be unavailable exactly when an operator
14//! reached for it.
15//!
16//! **What these numbers do not promise is in §12.2 and repeated on the types below.** The short
17//! version: each field is individually exact, and the *relationship* between two fields of one
18//! snapshot is not, because they are separate atomics read one after another.
19
20use std::sync::atomic::{AtomicU64, Ordering};
21
22use sipx_sip::Method;
23use sipx_sip::transaction::Timer;
24
25use crate::target::TransportKind;
26
27/// How many transports are counted apart. One slot per [`TransportKind`] variant.
28const TRANSPORTS: usize = 6;
29
30/// Which slot a transport's counters live in.
31///
32/// A match rather than `as usize`, so adding a `TransportKind` variant is a compile error here
33/// instead of a silent write into the wrong transport's numbers.
34const fn slot(transport: TransportKind) -> usize {
35    match transport {
36        TransportKind::Udp => 0,
37        TransportKind::Tcp => 1,
38        TransportKind::Tls => 2,
39        TransportKind::Ws => 3,
40        TransportKind::Wss => 4,
41        TransportKind::Quic => 5,
42    }
43}
44
45/// What the endpoint has dropped because the application was not keeping up.
46///
47/// Kept as atomics behind an `Arc` rather than answered by the event loop, and that is the point:
48/// the loop is busy in exactly the situation this counts, so a counter you could only read by
49/// asking it would be unreadable when it mattered. [`crate::Handle::shed`] reads it without
50/// touching the loop at all.
51///
52/// The three kinds are counted apart because their consequences differ by an order of magnitude,
53/// and one number would hide that.
54#[derive(Debug, Default)]
55pub(crate) struct Shed {
56    pub(crate) requests: AtomicU64,
57    pub(crate) acks: AtomicU64,
58    pub(crate) unmatched: AtomicU64,
59}
60
61/// A snapshot of what an endpoint has shed (see [`crate::Handle::shed`]).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub struct ShedCounts {
64    /// Requests that reached a server transaction and could not be handed to the application.
65    ///
66    /// Answered `503 Service Unavailable` with a `Retry-After`, so the peer is told something
67    /// true rather than left to retransmit into a queue that is still full.
68    pub requests: u64,
69    /// **ACKs** that could not be handed over.
70    ///
71    /// The serious one, and the reason these are not one number. An ACK for a 2xx has no
72    /// transaction to answer — RFC 3261 §17.1.1.3 makes it a new transaction of its own, and
73    /// there is no response to an ACK in SIP at all — so there is no 503 to send and nothing
74    /// retransmits it after Timer H. Both ends are then in a dialog that no timer will reap
75    /// unless session timers (RFC 4028) happen to be in play. A non-zero count here means calls
76    /// are leaking.
77    pub acks: u64,
78    /// Requests that matched no transaction and could not be handed over.
79    ///
80    /// The peer will retransmit an unmatched INVITE, so this is the most survivable of the three
81    /// — but it is still loss, and it was previously invisible.
82    pub unmatched: u64,
83}
84
85impl ShedCounts {
86    /// Whether anything has been shed at all.
87    #[must_use]
88    pub fn any(self) -> bool {
89        self.total() > 0
90    }
91
92    /// Everything shed, of every kind.
93    #[must_use]
94    pub fn total(self) -> u64 {
95        self.requests
96            .saturating_add(self.acks)
97            .saturating_add(self.unmatched)
98    }
99}
100
101/// Per-transport message counts, live.
102#[derive(Debug, Default)]
103struct TransportMeter {
104    requests_in: AtomicU64,
105    requests_out: AtomicU64,
106    responses_in: AtomicU64,
107    responses_out: AtomicU64,
108    parse_failures: AtomicU64,
109    source_refusals: AtomicU64,
110}
111
112/// What crossed one transport, in both directions.
113///
114/// Per transport because which transport is the first question a support case asks, and an
115/// aggregate cannot answer it: "we are losing messages" has a different cause over UDP than over a
116/// WebSocket, and the same total.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
118pub struct TransportCounts {
119    /// Requests that arrived and parsed.
120    pub requests_in: u64,
121    /// Requests put on the wire, including retransmissions.
122    pub requests_out: u64,
123    /// Responses that arrived and parsed.
124    pub responses_in: u64,
125    /// Responses put on the wire, including retransmissions.
126    pub responses_out: u64,
127    /// Bytes that arrived and could not be parsed as a SIP message.
128    ///
129    /// **Not** also counted as a request or a response: which one it would have been is exactly
130    /// what could not be determined (§12.2). So `requests_in + responses_in` omits these, and the
131    /// number of messages that arrived at all is that sum *plus* this.
132    pub parse_failures: u64,
133    /// Packets or new connections rejected by live source admission before protocol work.
134    pub source_refusals: u64,
135}
136
137/// Transactions abandoned by the timer that gave up on them.
138///
139/// Split by timer because the three mean different things to whoever is reading. A rise in `b` or
140/// `f` is a peer that stopped answering; a rise in `h` is a peer that answered and then never
141/// acknowledged our final response, which is a different fault with a different fix.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
143pub struct TimeoutCounts {
144    /// Timer B: an INVITE client transaction gave up (RFC 3261 §17.1.1.2).
145    pub b: u64,
146    /// Timer F: a non-INVITE client transaction gave up (§17.1.2.2).
147    pub f: u64,
148    /// Timer H: no ACK arrived for a final INVITE response (§17.2.1).
149    pub h: u64,
150}
151
152impl TimeoutCounts {
153    /// Every transaction any timer gave up on.
154    #[must_use]
155    pub fn total(self) -> u64 {
156        self.b.saturating_add(self.f).saturating_add(self.h)
157    }
158}
159
160/// How the capture is faring, if one is running (§13).
161///
162/// Zero on every field is the ordinary state, because capture is off by default.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
164pub struct CaptureCounts {
165    /// Records handed to the writer.
166    pub records: u64,
167    /// Records dropped because the writer was behind.
168    ///
169    /// The channel to the writer is bounded and an overrun drops rather than blocking the driver
170    /// (§13.2): blocking would put the filesystem in the retransmission path. A capture with a gap
171    /// that says so is usable; a stack that stalled to avoid the gap is not.
172    pub dropped: u64,
173    /// Writes that failed, after which the capture is disabled.
174    ///
175    /// A full disk is the usual reason. Counted rather than only logged because a capture that is
176    /// silently not happening is the same failure as a silent discard, one level up.
177    pub errors: u64,
178    /// Records sent to the configured HEP3 collector.
179    pub hep_records: u64,
180    /// HEP3 records rejected by encoding or the non-blocking collector socket.
181    ///
182    /// A drop never disables the local pcapng capture or fails a call.
183    pub hep_dropped: u64,
184}
185
186/// Places the endpoint throws something away that are not backpressure (§12.1).
187///
188/// Each of these was a `let _ = …` or a bare `tracing` line before `X-18`. None is necessarily a
189/// fault — most are the correct handling of something unwanted — but every one of them used to be
190/// invisible, and "how often" is not a question anyone should answer with `grep | wc -l`.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
192pub struct DiscardCounts {
193    /// Events for a client transaction whose application receiver was full or gone.
194    ///
195    /// The serious one here. A dropped response event means an application that asked for a
196    /// transaction's outcome does not learn it, and nothing retransmits an event.
197    pub transaction_events: u64,
198    /// Server transactions abandoned because the application never answered them.
199    ///
200    /// An application bug rather than a network one, which is exactly why it needs its own number:
201    /// nothing on the wire will show it.
202    pub unanswered: u64,
203    /// Messages a transaction wanted sent that had no destination to send them to.
204    pub no_destination: u64,
205    /// Sends the transport refused.
206    ///
207    /// The transaction is given a transport error, so this is not silent loss — but the rate is
208    /// worth having, because a peer that has become unreachable shows up here first.
209    pub send_failures: u64,
210    /// STUN replies that matched no keep-alive, and STUN messages that were not replies.
211    ///
212    /// RFC 5389 §6 wants the transaction ID unguessable precisely so a forged reply cannot be
213    /// matched; a rising count here is either a broken peer or someone trying.
214    pub stun_unmatched: u64,
215}
216
217impl DiscardCounts {
218    /// Everything discarded outside the backpressure path.
219    #[must_use]
220    pub fn total(self) -> u64 {
221        self.transaction_events
222            .saturating_add(self.unanswered)
223            .saturating_add(self.no_destination)
224            .saturating_add(self.send_failures)
225            .saturating_add(self.stun_unmatched)
226    }
227}
228
229/// Live counts of requests the endpoint was asked to send and did not.
230#[derive(Debug, Default)]
231struct Unsent {
232    invite: AtomicU64,
233    ack: AtomicU64,
234    bye: AtomicU64,
235    cancel: AtomicU64,
236    other: AtomicU64,
237}
238
239/// Requests the endpoint tried to put on the wire and could not, by method (§12.3).
240///
241/// **Split by method because the consequence is.** A CANCEL that does not go out leaves the far end
242/// ringing; an ACK that does not go out leaves a 2xx retransmitting for thirty-two seconds and then
243/// a peer streaming at a port this side has closed; a BYE that does not go out leaves a dialog up
244/// at the far end that no timer will reap. One number would hide which of those happened, and they
245/// are the three questions an operator asking "why did that call linger" is choosing between.
246///
247/// # Where this is counted, and why not at the hand-off
248///
249/// **At the transmit, in the driver** — the two places the socket is actually written: a
250/// transaction's `Output::Send`, and the direct send that carries an ACK for a 2xx.
251///
252/// It was counted inside [`crate::Handle::send`] and [`crate::Handle::send_directly`] when `X-54`
253/// first wrote it, and that was **wrong in a way the type's own documentation concealed**.
254/// `Handle::send` returns as soon as the driver has created the transaction and handed back its
255/// key; the transmit happens afterwards. So a counter at that hand-off could only ever fire when
256/// the endpoint refused the request outright — a closed endpoint, or a request with no usable
257/// `Via` — and **never** on a refused connection, an unreachable peer or an over-MTU datagram,
258/// which is the whole of the question it claims to answer. `send_directly` did await the transmit,
259/// so `ack` behaved one way and `bye` and `cancel` another, with nothing saying so. Counting where
260/// the wire is missed makes all four mean the same thing.
261///
262/// # What this does and does not promise (§12.2)
263///
264/// - **Requests only.** A response that fails to transmit is counted by
265///   [`DiscardCounts::send_failures`] and not here, because these fields are methods and a response
266///   has none.
267/// - **This overlaps [`DiscardCounts::send_failures`] on purpose, and the two are views rather
268///   than tallies.** A *request* that fails on the transaction path increments both: that field is
269///   the transaction path's aggregate over requests and responses alike, this is the per-method
270///   breakdown over requests on any path. Adding them together is meaningless, and so is
271///   subtracting them — §12.2's rule against arithmetic across fields applies here in particular.
272/// - **An endpoint that is shutting down does not inflate this.** A send that loses the race with
273///   `shutdown` fails at the hand-off, before any transmit is attempted, and is not counted — the
274///   earlier design counted it, which made an ordinary teardown look like lost signalling.
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
276pub struct UnsentCounts {
277    /// INVITEs the endpoint could not put on the wire.
278    pub invite: u64,
279    /// ACKs the endpoint could not put on the wire — RFC 3261 §13.2.2.4's ACK for a 2xx, which has
280    /// no transaction to retry it.
281    pub ack: u64,
282    /// BYEs the endpoint could not put on the wire. The one that leaves a call up at the far end.
283    pub bye: u64,
284    /// CANCELs the endpoint could not put on the wire. The one that leaves a phone ringing.
285    pub cancel: u64,
286    /// Every other method the endpoint could not put on the wire.
287    pub other: u64,
288}
289
290impl UnsentCounts {
291    /// Every request that did not reach the wire, of every method.
292    #[must_use]
293    pub fn total(self) -> u64 {
294        self.invite
295            .saturating_add(self.ack)
296            .saturating_add(self.bye)
297            .saturating_add(self.cancel)
298            .saturating_add(self.other)
299    }
300}
301
302/// Everything an endpoint will tell you about itself, at one moment (§12).
303///
304/// # What this is not
305///
306/// **A snapshot is not an instant.** The fields are separate atomics read one after another, so a
307/// snapshot taken while traffic flows can show [`TransportCounts::requests_in`] from a later moment
308/// than [`TransportCounts::responses_in`]. Every field is individually monotonic and none is ever
309/// lost, so *differences between successive snapshots* are sound. Arithmetic identities across
310/// fields of a single snapshot are not, unless the endpoint is quiet.
311///
312/// See [`TransportCounts::parse_failures`] for the one place that surprises people: in and out do
313/// not balance, by construction.
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
315pub struct Counters {
316    /// What was dropped because the application was not keeping up (§10).
317    ///
318    /// The same value [`crate::Handle::shed`] returns, embedded rather than recounted: two tallies
319    /// of one event would eventually disagree, and then neither could be trusted.
320    pub shed: ShedCounts,
321    /// Locally rejected outbound requests under RFC 7339/RFC 7415 control.
322    pub overload_rejections: u64,
323    /// Responses that matched no client transaction (RFC 3261 §16.7).
324    ///
325    /// Counted whether or not anyone is watching for them through
326    /// [`crate::Handle::watch_unmatched`]. A user agent is right to ignore these; a forwarding
327    /// element is required to act on them, and either way the rate is worth knowing.
328    pub unmatched_responses: u64,
329    /// Retransmissions put on the wire by Timer A, E or G.
330    ///
331    /// Counted **where the timer fires**, so a retransmission the socket then refuses is still
332    /// counted as sent (§12.2). Counting after the socket call would mean a peer that stopped
333    /// hearing us produced a *falling* count, inverting the signal this exists to give.
334    ///
335    /// A rise with no matching growth in traffic is a peer that is not hearing us, which is the
336    /// difference between a network problem and an application one.
337    pub retransmissions_sent: u64,
338    /// Oversized UDP requests for which RFC 3261 §18.1.1 selected TCP instead.
339    ///
340    /// Counted at selection, including a selection whose connection attempt then fails. Consult
341    /// [`Self::unsent`] and [`Self::discards`] for whether the selected send reached the wire.
342    pub oversized_request_tcp_fallbacks: u64,
343    /// Transactions a timer gave up on.
344    pub timeouts: TimeoutCounts,
345    /// Discards that are not backpressure (§12.1).
346    pub discards: DiscardCounts,
347    /// Requests the endpoint tried to put on the wire and could not, by method (§12.3).
348    ///
349    /// Overlaps [`DiscardCounts::send_failures`] for requests on the transaction path — see
350    /// [`UnsentCounts`], which states what that does and does not let you conclude.
351    pub unsent: UnsentCounts,
352    /// How the capture is faring, if one is running (§13).
353    pub capture: CaptureCounts,
354    /// Observation events dropped because the optional bounded consumer was behind.
355    pub observation_dropped: u64,
356    /// Per-transport message counts, read through [`Counters::transport`].
357    per_transport: [TransportCounts; TRANSPORTS],
358}
359
360impl Counters {
361    /// What crossed one transport.
362    #[must_use]
363    pub fn transport(&self, transport: TransportKind) -> TransportCounts {
364        // `slot` is total over the enum and `per_transport` is sized to match, so this cannot be
365        // out of range — but `get` says so without a panicking index (`AGENTS.md` §3).
366        self.per_transport
367            .get(slot(transport))
368            .copied()
369            .unwrap_or_default()
370    }
371
372    /// Messages that arrived and parsed, over every transport.
373    #[must_use]
374    pub fn messages_in(&self) -> u64 {
375        self.per_transport.iter().fold(0, |total, counts| {
376            total
377                .saturating_add(counts.requests_in)
378                .saturating_add(counts.responses_in)
379        })
380    }
381
382    /// Messages put on the wire, over every transport.
383    #[must_use]
384    pub fn messages_out(&self) -> u64 {
385        self.per_transport.iter().fold(0, |total, counts| {
386            total
387                .saturating_add(counts.requests_out)
388                .saturating_add(counts.responses_out)
389        })
390    }
391
392    /// Everything that arrived and could not be parsed, over every transport.
393    #[must_use]
394    pub fn parse_failures(&self) -> u64 {
395        self.per_transport.iter().fold(0, |total, counts| {
396            total.saturating_add(counts.parse_failures)
397        })
398    }
399
400    /// Whether anything at all has been lost: shed, discarded, dropped from a capture, or never
401    /// put on the wire.
402    ///
403    /// A single question for a health check to ask. Deliberately does **not** include
404    /// [`Self::parse_failures`] or [`Self::timeouts`]: a malformed datagram from a stranger and a
405    /// peer that stopped answering are things that happened *to* the endpoint, not things it threw
406    /// away, and folding them in here would make the number impossible to act on.
407    #[must_use]
408    pub fn any_loss(&self) -> bool {
409        self.shed.any()
410            || self.overload_rejections > 0
411            || self.discards.total() > 0
412            || self.capture.dropped > 0
413            || self.capture.hep_dropped > 0
414            || self.observation_dropped > 0
415            || self.unsent.total() > 0
416    }
417}
418
419/// The live counters, shared between the driver and every handle.
420///
421/// Every counter below is incremented from exactly one place in the crate — the methods on this
422/// type — which is what makes §12.2's promise checkable: there is no path on which one event
423/// increments a counter twice, and none on which an increment is lost. `Relaxed` throughout,
424/// because nothing here guards data and no reader draws a conclusion from the order of two
425/// increments.
426#[derive(Debug, Default)]
427pub(crate) struct Meters {
428    pub(crate) shed: Shed,
429    overload_rejections: AtomicU64,
430    per_transport: [TransportMeter; TRANSPORTS],
431    unmatched_responses: AtomicU64,
432    retransmissions: AtomicU64,
433    oversized_request_tcp_fallbacks: AtomicU64,
434    timeout_b: AtomicU64,
435    timeout_f: AtomicU64,
436    timeout_h: AtomicU64,
437    discard_transaction_events: AtomicU64,
438    discard_unanswered: AtomicU64,
439    discard_no_destination: AtomicU64,
440    discard_send_failures: AtomicU64,
441    discard_stun_unmatched: AtomicU64,
442    capture_records: AtomicU64,
443    capture_dropped: AtomicU64,
444    capture_errors: AtomicU64,
445    capture_hep_records: AtomicU64,
446    capture_hep_dropped: AtomicU64,
447    observation_dropped: AtomicU64,
448    unsent: Unsent,
449}
450
451/// One increment, in the one ordering this module uses.
452fn bump(counter: &AtomicU64) {
453    counter.fetch_add(1, Ordering::Relaxed);
454}
455
456fn read(counter: &AtomicU64) -> u64 {
457    counter.load(Ordering::Relaxed)
458}
459
460impl Meters {
461    /// An outbound request was rejected by active overload control.
462    pub(crate) fn overload_rejection(&self) {
463        bump(&self.overload_rejections);
464    }
465
466    /// The meter for one transport.
467    fn meter(&self, transport: TransportKind) -> Option<&TransportMeter> {
468        self.per_transport.get(slot(transport))
469    }
470
471    /// A message arrived and parsed.
472    pub(crate) fn message_in(&self, transport: TransportKind, is_response: bool) {
473        if let Some(meter) = self.meter(transport) {
474            if is_response {
475                bump(&meter.responses_in);
476            } else {
477                bump(&meter.requests_in);
478            }
479        }
480    }
481
482    /// A message went out.
483    pub(crate) fn message_out(&self, transport: TransportKind, is_response: bool) {
484        if let Some(meter) = self.meter(transport) {
485            if is_response {
486                bump(&meter.responses_out);
487            } else {
488                bump(&meter.requests_out);
489            }
490        }
491    }
492
493    /// Bytes arrived that were not a SIP message.
494    pub(crate) fn parse_failure(&self, transport: TransportKind) {
495        if let Some(meter) = self.meter(transport) {
496            bump(&meter.parse_failures);
497        }
498    }
499
500    /// A source was refused before parsing or handshaking.
501    pub(crate) fn source_refusal(&self, transport: TransportKind) {
502        if let Some(meter) = self.meter(transport) {
503            bump(&meter.source_refusals);
504        }
505    }
506
507    /// A bounded observation receiver was full.
508    pub(crate) fn observation_drop(&self) {
509        bump(&self.observation_dropped);
510    }
511
512    /// A response matched no client transaction.
513    pub(crate) fn unmatched_response(&self) {
514        bump(&self.unmatched_responses);
515    }
516
517    /// A timer fired and produced a retransmission.
518    ///
519    /// Only A, E and G retransmit; every other timer is a deadline or an absorption window, and
520    /// counting those here would make the number mean "timers fired" instead.
521    pub(crate) fn on_timer(&self, timer: Timer) {
522        match timer {
523            Timer::A | Timer::E | Timer::G => bump(&self.retransmissions),
524            Timer::B => bump(&self.timeout_b),
525            Timer::F => bump(&self.timeout_f),
526            Timer::H => bump(&self.timeout_h),
527            Timer::D | Timer::I | Timer::J | Timer::K | Timer::L | Timer::M | Timer::Trying100 => {}
528        }
529    }
530
531    /// An oversized UDP request selected TCP under RFC 3261 §18.1.1.
532    pub(crate) fn oversized_request_tcp_fallback(&self) {
533        bump(&self.oversized_request_tcp_fallbacks);
534    }
535
536    /// An event for a client transaction could not be handed over.
537    pub(crate) fn discard_transaction_event(&self) {
538        bump(&self.discard_transaction_events);
539    }
540
541    /// A server transaction the application never answered was abandoned.
542    pub(crate) fn discard_unanswered(&self) {
543        bump(&self.discard_unanswered);
544    }
545
546    /// A message a transaction wanted sent had nowhere to go.
547    pub(crate) fn discard_no_destination(&self) {
548        bump(&self.discard_no_destination);
549    }
550
551    /// The transport refused a send.
552    pub(crate) fn discard_send_failure(&self) {
553        bump(&self.discard_send_failures);
554    }
555
556    /// A STUN message matched no keep-alive, or was not a reply.
557    pub(crate) fn discard_stun_unmatched(&self) {
558        bump(&self.discard_stun_unmatched);
559    }
560
561    /// A record was handed to the capture writer.
562    pub(crate) fn capture_record(&self) {
563        bump(&self.capture_records);
564    }
565
566    /// A record was dropped because the capture writer was behind.
567    pub(crate) fn capture_drop(&self) {
568        bump(&self.capture_dropped);
569    }
570
571    /// A capture write failed.
572    pub(crate) fn capture_error(&self) {
573        bump(&self.capture_errors);
574    }
575
576    /// A redacted HEP3 datagram reached the collector socket.
577    pub(crate) fn capture_hep_record(&self) {
578        bump(&self.capture_hep_records);
579    }
580
581    /// A HEP3 datagram could not be encoded or sent without blocking.
582    pub(crate) fn capture_hep_drop(&self) {
583        bump(&self.capture_hep_dropped);
584    }
585
586    /// A request the endpoint tried to put on the wire and could not.
587    ///
588    /// Called from the driver, at the two places the socket is written — never from a `Handle`
589    /// method, which returns before the transmit happens (see [`UnsentCounts`]).
590    ///
591    /// A match rather than an index, so a new [`Method`] variant is a compile error here instead of
592    /// silently landing in `other` — the same reason [`slot`] is a match.
593    pub(crate) fn unsent(&self, method: &Method) {
594        bump(match method {
595            Method::Invite => &self.unsent.invite,
596            Method::Ack => &self.unsent.ack,
597            Method::Bye => &self.unsent.bye,
598            Method::Cancel => &self.unsent.cancel,
599            Method::Register
600            | Method::Options
601            | Method::Info
602            | Method::Prack
603            | Method::Update
604            | Method::Subscribe
605            | Method::Notify
606            | Method::Refer
607            | Method::Message
608            | Method::Publish
609            | Method::Other(_) => &self.unsent.other,
610        });
611    }
612
613    /// Read everything, field by field.
614    ///
615    /// Not a consistent instant, and [`Counters`] says so: taking a lock to make it one would put
616    /// the reader in the driver's way, which is the thing §12 refuses to do.
617    pub(crate) fn snapshot(&self) -> Counters {
618        let mut per_transport = [TransportCounts::default(); TRANSPORTS];
619        for (counts, meter) in per_transport.iter_mut().zip(self.per_transport.iter()) {
620            *counts = TransportCounts {
621                requests_in: read(&meter.requests_in),
622                requests_out: read(&meter.requests_out),
623                responses_in: read(&meter.responses_in),
624                responses_out: read(&meter.responses_out),
625                parse_failures: read(&meter.parse_failures),
626                source_refusals: read(&meter.source_refusals),
627            };
628        }
629        Counters {
630            shed: ShedCounts {
631                requests: read(&self.shed.requests),
632                acks: read(&self.shed.acks),
633                unmatched: read(&self.shed.unmatched),
634            },
635            overload_rejections: read(&self.overload_rejections),
636            unmatched_responses: read(&self.unmatched_responses),
637            retransmissions_sent: read(&self.retransmissions),
638            oversized_request_tcp_fallbacks: read(&self.oversized_request_tcp_fallbacks),
639            timeouts: TimeoutCounts {
640                b: read(&self.timeout_b),
641                f: read(&self.timeout_f),
642                h: read(&self.timeout_h),
643            },
644            discards: DiscardCounts {
645                transaction_events: read(&self.discard_transaction_events),
646                unanswered: read(&self.discard_unanswered),
647                no_destination: read(&self.discard_no_destination),
648                send_failures: read(&self.discard_send_failures),
649                stun_unmatched: read(&self.discard_stun_unmatched),
650            },
651            capture: CaptureCounts {
652                records: read(&self.capture_records),
653                dropped: read(&self.capture_dropped),
654                errors: read(&self.capture_errors),
655                hep_records: read(&self.capture_hep_records),
656                hep_dropped: read(&self.capture_hep_dropped),
657            },
658            observation_dropped: read(&self.observation_dropped),
659            unsent: UnsentCounts {
660                invite: read(&self.unsent.invite),
661                ack: read(&self.unsent.ack),
662                bye: read(&self.unsent.bye),
663                cancel: read(&self.unsent.cancel),
664                other: read(&self.unsent.other),
665            },
666            per_transport,
667        }
668    }
669}
670
671#[cfg(test)]
672#[allow(
673    clippy::unwrap_used,
674    clippy::expect_used,
675    clippy::panic,
676    clippy::indexing_slicing
677)]
678mod tests {
679    use super::*;
680
681    #[test]
682    fn every_transport_has_its_own_slot() {
683        let kinds = [
684            TransportKind::Udp,
685            TransportKind::Tcp,
686            TransportKind::Tls,
687            TransportKind::Ws,
688            TransportKind::Wss,
689            TransportKind::Quic,
690        ];
691        let mut slots: Vec<usize> = kinds.iter().copied().map(slot).collect();
692        slots.sort_unstable();
693        slots.dedup();
694        assert_eq!(
695            slots.len(),
696            TRANSPORTS,
697            "two transports share a slot, so their counts are being added together"
698        );
699        assert_eq!(
700            slots.last().copied(),
701            Some(TRANSPORTS - 1),
702            "a slot is out of range of the array it indexes"
703        );
704    }
705
706    #[test]
707    fn a_message_is_counted_against_its_own_transport_only() {
708        let meters = Meters::default();
709        meters.message_in(TransportKind::Tcp, false);
710        meters.message_out(TransportKind::Tcp, true);
711
712        let counters = meters.snapshot();
713        assert_eq!(counters.transport(TransportKind::Tcp).requests_in, 1);
714        assert_eq!(counters.transport(TransportKind::Tcp).responses_out, 1);
715        assert_eq!(
716            counters.transport(TransportKind::Udp),
717            TransportCounts::default(),
718            "a TCP message must not appear in UDP's counts"
719        );
720        assert_eq!(counters.messages_in(), 1);
721        assert_eq!(counters.messages_out(), 1);
722    }
723
724    /// §12.2's second limit: a parse failure is not also a message.
725    #[test]
726    fn a_parse_failure_is_not_counted_as_a_message() {
727        let meters = Meters::default();
728        meters.parse_failure(TransportKind::Udp);
729
730        let counters = meters.snapshot();
731        assert_eq!(counters.parse_failures(), 1);
732        assert_eq!(
733            counters.messages_in(),
734            0,
735            "which it would have been is exactly what could not be determined"
736        );
737    }
738
739    /// Only the three retransmission timers count as retransmissions, and only the three deadline
740    /// timers as timeouts. Without this the absorption timers — D, I, J, K, L, M — would inflate
741    /// both numbers on every ordinary transaction.
742    #[test]
743    fn only_the_retransmission_timers_count_as_retransmissions() {
744        let meters = Meters::default();
745        for timer in [Timer::A, Timer::E, Timer::G] {
746            meters.on_timer(timer);
747        }
748        for timer in [
749            Timer::D,
750            Timer::I,
751            Timer::J,
752            Timer::K,
753            Timer::L,
754            Timer::M,
755            Timer::Trying100,
756        ] {
757            meters.on_timer(timer);
758        }
759
760        let counters = meters.snapshot();
761        assert_eq!(counters.retransmissions_sent, 3);
762        assert_eq!(
763            counters.timeouts.total(),
764            0,
765            "an absorption window closing is not a transaction timing out"
766        );
767    }
768
769    #[test]
770    fn each_deadline_timer_is_counted_apart() {
771        let meters = Meters::default();
772        meters.on_timer(Timer::B);
773        meters.on_timer(Timer::H);
774        meters.on_timer(Timer::H);
775
776        let counters = meters.snapshot();
777        assert_eq!(counters.timeouts.b, 1);
778        assert_eq!(counters.timeouts.f, 0);
779        assert_eq!(counters.timeouts.h, 2);
780        assert_eq!(counters.timeouts.total(), 3);
781    }
782
783    /// `any_loss` is what a health check asks. It must answer "yes" to a discard and "no" to a
784    /// malformed datagram, which happened *to* the endpoint rather than being thrown away by it.
785    #[test]
786    fn any_loss_covers_discards_and_not_arrivals() {
787        let meters = Meters::default();
788        assert!(!meters.snapshot().any_loss());
789
790        meters.parse_failure(TransportKind::Udp);
791        meters.on_timer(Timer::B);
792        assert!(
793            !meters.snapshot().any_loss(),
794            "a stranger's malformed datagram is not this endpoint losing something"
795        );
796
797        meters.discard_transaction_event();
798        assert!(meters.snapshot().any_loss());
799    }
800
801    #[test]
802    fn a_fresh_endpoint_reports_zero_everywhere() {
803        let counters = Meters::default().snapshot();
804        assert_eq!(counters, Counters::default());
805        assert!(!counters.any_loss());
806        assert_eq!(counters.capture, CaptureCounts::default());
807    }
808}