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}