Skip to main content

pb_mapper_client/
diagnostics.rs

1//! Bounded, credential-free recovery counters exposed separately from lifecycle status.
2//!
3//! Besides counters, a tunnel keeps the direct attempt it is making right now
4//! and a short history of what happened to it ([`TunnelEvent`]), so a caller can
5//! show recovery as it unfolds instead of a bare "retrying"; [`TunnelEvents`]
6//! delivers each event as it happens. All are written only on control-path
7//! transitions, never per packet.
8
9use crate::endpoint::RelayEndpoint;
10use pb_mapper_protocol::direct::SessionPath;
11use serde::Serialize;
12use std::collections::VecDeque;
13use std::sync::{Arc, Mutex};
14use tokio::sync::broadcast;
15use tokio::time::{Duration, Instant};
16
17mod port_mapping;
18pub use port_mapping::{
19    PortMappingDiagnostics, PortMappingProtocol, PortMappingStage, PortMappingStatus,
20};
21
22/// The phase most recently entered by a tunnel worker.
23#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
24#[serde(rename_all = "snake_case")]
25pub enum RecoveryPhase {
26    Starting,
27    Capacity,
28    Resolving,
29    Connecting,
30    Handshake,
31    Ready,
32    Backoff,
33    Stopped,
34}
35
36impl RecoveryPhase {
37    /// Stable diagnostic label, matching the serialized representation.
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Self::Starting => "starting",
41            Self::Capacity => "capacity",
42            Self::Resolving => "resolving",
43            Self::Connecting => "connecting",
44            Self::Handshake => "handshake",
45            Self::Ready => "ready",
46            Self::Backoff => "backoff",
47            Self::Stopped => "stopped",
48        }
49    }
50}
51
52/// Why a recovery attempt failed, without credentials or application data.
53#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum RecoveryFailure {
56    Dns,
57    Transport,
58    Timeout,
59    ServiceUnavailable,
60    Rejected,
61    NetworkChanged,
62}
63
64impl RecoveryFailure {
65    /// Stable diagnostic label, matching the serialized representation.
66    pub fn as_str(self) -> &'static str {
67        match self {
68            Self::Dns => "dns",
69            Self::Transport => "transport",
70            Self::Timeout => "timeout",
71            Self::ServiceUnavailable => "service_unavailable",
72            Self::Rejected => "rejected",
73            Self::NetworkChanged => "network_changed",
74        }
75    }
76}
77
78/// Where a direct attempt stands. Every stage before [`Self::Committing`] can
79/// still fall back to the relay; a fallback names the stage it left.
80#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
81#[serde(rename_all = "snake_case")]
82pub enum DirectStage {
83    /// Asking the relay whether the publisher offers direct, and opening the
84    /// control link.
85    Negotiating,
86    /// Learning this endpoint's public address and requesting a session grant.
87    Discovering,
88    /// Dialing the publisher through both NATs.
89    Punching,
90    /// The path is up; the publisher is opening its local service.
91    Committing,
92}
93
94impl DirectStage {
95    /// Stable diagnostic label, matching the serialized representation.
96    pub fn as_str(self) -> &'static str {
97        match self {
98            Self::Negotiating => "negotiating",
99            Self::Discovering => "discovering",
100            Self::Punching => "punching",
101            Self::Committing => "committing",
102        }
103    }
104}
105
106/// The direct attempt a tunnel is making right now.
107#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
108pub struct DirectAttempt {
109    /// The stage most recently entered by any attempt in flight.
110    pub stage: DirectStage,
111    /// Time since that attempt began.
112    pub elapsed_ms: u64,
113    /// Attempts in flight, this one included.
114    pub in_flight: u32,
115}
116
117/// What happened, in a [`TunnelEvent`].
118///
119/// The first six are outcomes, kept in [`TunnelDiagnostics::events`] as well
120/// as delivered to [`TunnelEvents`]. The rest are steps along the way,
121/// delivered live only: kept, they would crowd the outcomes out of a history
122/// of 16.
123#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
124#[serde(tag = "kind", rename_all = "snake_case")]
125pub enum TunnelEventKind {
126    /// The relay answered for the first time, or again after failures.
127    RelayReady { latency_ms: u64 },
128    /// A relay attempt failed; the next one is `retry_in_ms` away (zero when
129    /// the worker retries at once or the failure is final).
130    RelayFailed {
131        failure: RecoveryFailure,
132        retry_in_ms: u64,
133    },
134    /// The local network changed; recovery restarts at once.
135    NetworkChanged,
136    /// A session took the direct path.
137    DirectEstablished { setup_ms: u64 },
138    /// A session gave up on direct at `stage` and used the relay.
139    DirectFallback { stage: DirectStage },
140    /// The tunnel stopped, and whatever it was cleaning up has reported. The
141    /// last event of every stream.
142    Stopped,
143    /// A relay attempt entered `phase`. Once the tunnel is ready, its routine
144    /// re-checks are not announced: the next change is `backoff`, when one
145    /// fails.
146    PhaseChanged { phase: RecoveryPhase },
147    /// A direct attempt entered `stage`.
148    DirectStageChanged { stage: DirectStage },
149    /// A direct attempt ended at `stage` with neither a direct session nor a
150    /// fallback: its setup was cancelled, or it failed after committing, when
151    /// the relay can no longer take its place.
152    DirectAbandoned { stage: DirectStage },
153    /// A session opened on `path`.
154    SessionOpened { path: SessionPath },
155    /// A session on `path` closed.
156    SessionClosed { path: SessionPath },
157    /// The gateway mapping of the tunnel's direct endpoint changed.
158    PortMappingChanged { mapping: Box<PortMappingStatus> },
159}
160
161impl TunnelEventKind {
162    /// Whether a repeat of `self` folds into the entry for `other`.
163    fn folds_into(&self, other: &Self) -> bool {
164        match (self, other) {
165            (Self::RelayFailed { failure: a, .. }, Self::RelayFailed { failure: b, .. }) => a == b,
166            (Self::DirectFallback { stage: a }, Self::DirectFallback { stage: b }) => a == b,
167            (Self::RelayReady { .. }, Self::RelayReady { .. })
168            | (Self::NetworkChanged, Self::NetworkChanged)
169            | (Self::DirectEstablished { .. }, Self::DirectEstablished { .. }) => true,
170            _ => false,
171        }
172    }
173
174    /// Whether this is a step delivered live only, never kept in the history.
175    fn is_live_only(&self) -> bool {
176        matches!(
177            self,
178            Self::PhaseChanged { .. }
179                | Self::DirectStageChanged { .. }
180                | Self::DirectAbandoned { .. }
181                | Self::SessionOpened { .. }
182                | Self::SessionClosed { .. }
183                | Self::PortMappingChanged { .. }
184        )
185    }
186}
187
188/// One event of a tunnel. In [`TunnelDiagnostics::events`], oldest first,
189/// consecutive repeats fold into one entry, so a long outage cannot push out
190/// what came before it; [`TunnelEvents`] delivers every occurrence.
191#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
192pub struct TunnelEvent {
193    /// Increases with every event, a fold included, so a reader can tell
194    /// what it has not seen yet. A stream and the history share it.
195    pub seq: u64,
196    /// Time since the latest occurrence.
197    pub age_ms: u64,
198    /// Occurrences this entry stands for.
199    pub repeats: u32,
200    /// The latest occurrence.
201    pub kind: TunnelEventKind,
202}
203
204/// How many entries a tunnel's history keeps.
205const EVENT_CAPACITY: usize = 16;
206
207/// How many events a subscriber may fall behind before it misses some.
208const STREAM_CAPACITY: usize = 128;
209
210/// A tunnel's events as they happen, after the history it has kept: from
211/// `subscribe_events` on a [`Registration`](crate::sdk::Registration) or a
212/// [`Connection`](crate::sdk::Connection).
213///
214/// Every subscriber reads at its own pace from its own queue. One that falls
215/// more than 128 events behind is told how many it missed, and goes on from
216/// there. A subscription does not keep its tunnel alive: once the tunnel
217/// stops and its cleanup has reported, the stream ends.
218pub struct TunnelEvents {
219    replay: VecDeque<Event>,
220    receiver: broadcast::Receiver<Event>,
221}
222
223impl TunnelEvents {
224    /// The next event, or `None` once the stream has ended. An error says
225    /// events were missed; the tunnel's `diagnostics()` is the state to
226    /// reconcile with, and the stream goes on. Cancel-safe.
227    pub async fn recv(&mut self) -> Option<Result<TunnelEvent, EventsLagged>> {
228        // Aged as it is read, however long it waited in the queue.
229        if let Some(event) = self.replay.pop_front() {
230            return Some(Ok(event.public(Instant::now())));
231        }
232        match self.receiver.recv().await {
233            Ok(event) => Some(Ok(event.public(Instant::now()))),
234            Err(broadcast::error::RecvError::Lagged(skipped)) => {
235                Some(Err(EventsLagged { skipped }))
236            }
237            Err(broadcast::error::RecvError::Closed) => None,
238        }
239    }
240}
241
242/// A [`TunnelEvents`] subscriber fell behind and missed `skipped` events.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub struct EventsLagged {
245    pub skipped: u64,
246}
247
248impl std::fmt::Display for EventsLagged {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        write!(f, "missed {} tunnel events", self.skipped)
251    }
252}
253
254impl std::error::Error for EventsLagged {}
255
256/// A point-in-time view of this tunnel and its process-shared relay budget.
257/// Non-exhaustive: fields are added as the SDK observes more.
258#[derive(Clone, Debug, Serialize)]
259#[non_exhaustive]
260pub struct TunnelDiagnostics {
261    /// The carrier this tunnel reaches the relay over, fixed when it started.
262    pub relay_transport: crate::relay::RelayTransport,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub direct: Option<PathDiagnostics>,
265    /// Present when the tunnel maps its direct endpoint's port: asked for,
266    /// with direct allowed.
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub port_mapping: Option<PortMappingDiagnostics>,
269    pub sdk_version: &'static str,
270    /// Most recent worker phase; use tunnel status for aggregate readiness.
271    pub last_attempt_phase: RecoveryPhase,
272    pub attempts: u64,
273    pub consecutive_failures: u64,
274    pub last_failure: Option<RecoveryFailure>,
275    /// Time since the last authenticated relay reply, even a negative reply.
276    pub last_success_age_ms: Option<u64>,
277    /// Last answered setup latency, excluding admission queue time.
278    pub last_setup_latency_ms: Option<u64>,
279    pub next_retry_in_ms: Option<u64>,
280    pub dns_age_ms: Option<u64>,
281    pub network_generation: u64,
282    /// Shared across this process for the same configured relay address.
283    pub active_control_setups: usize,
284    pub active_data_setups: usize,
285    /// The latest outcomes, oldest first; at most 16 entries. Steps along the
286    /// way are only in [`TunnelEvents`]; see [`TunnelEventKind`].
287    pub events: Vec<TunnelEvent>,
288}
289
290#[derive(Clone, Debug, Default, Serialize)]
291pub struct PathDiagnostics {
292    pub direct_sessions: u64,
293    pub relay_sessions: u64,
294    pub active_direct: u64,
295    pub active_relay: u64,
296    pub fallbacks: u64,
297    pub last_fallback: Option<String>,
298    /// The direct attempt in flight, if any.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub attempt: Option<DirectAttempt>,
301}
302
303#[derive(Clone)]
304struct Event {
305    seq: u64,
306    at: Instant,
307    repeats: u32,
308    kind: TunnelEventKind,
309}
310
311impl Event {
312    fn public(&self, now: Instant) -> TunnelEvent {
313        TunnelEvent {
314            seq: self.seq,
315            age_ms: ms(now.saturating_duration_since(self.at)),
316            repeats: self.repeats,
317            kind: self.kind.clone(),
318        }
319    }
320}
321
322/// How far a tunnel is from the end of its event stream.
323#[derive(Clone, Copy, PartialEq, Eq)]
324enum Lifecycle {
325    Running,
326    /// Stopped, while a cleanup that reports may still be running.
327    Stopping,
328    /// The stream has ended.
329    Closed,
330}
331
332struct State {
333    lifecycle: Lifecycle,
334    /// Cleanups that hold the stream open past a stop.
335    cleanups: usize,
336    /// Present while someone subscribes: a tunnel nobody listens to sends
337    /// nothing, and records no live-only event.
338    stream: Option<broadcast::Sender<Event>>,
339    direct: Option<PathDiagnostics>,
340    port_mapping: Option<PortMappingDiagnostics>,
341    phase: RecoveryPhase,
342    /// The phase last announced to subscribers. A ready tunnel's routine
343    /// re-checks pass through the attempt phases without being announced.
344    announced: RecoveryPhase,
345    attempts: u64,
346    failures: u64,
347    failure: Option<RecoveryFailure>,
348    success: Option<Instant>,
349    latency: Option<Duration>,
350    retry: Option<Instant>,
351    /// Whether the last relay attempt succeeded, so only a change is recorded.
352    ready: bool,
353    /// The network generation last seen, so observers of one change record
354    /// it once and a worker starting on an old one records nothing.
355    network: Option<u64>,
356    /// The stage and start of the latest direct attempt still in flight.
357    attempt: Option<(DirectStage, Instant)>,
358    in_flight: u32,
359    seq: u64,
360    events: VecDeque<Event>,
361}
362
363impl State {
364    fn record(&mut self, kind: TunnelEventKind) {
365        let live_only = kind.is_live_only();
366        if self.lifecycle == Lifecycle::Closed || (live_only && self.stream.is_none()) {
367            return;
368        }
369        self.seq = self.seq.wrapping_add(1);
370        let event = Event {
371            seq: self.seq,
372            at: Instant::now(),
373            repeats: 1,
374            kind,
375        };
376        if live_only {
377            self.send(event);
378            return;
379        }
380        if self.stream.is_some() {
381            self.send(event.clone());
382        }
383        if let Some(last) = self
384            .events
385            .back_mut()
386            .filter(|last| event.kind.folds_into(&last.kind))
387        {
388            *last = Event {
389                repeats: last.repeats.saturating_add(1),
390                ..event
391            };
392            return;
393        }
394        if self.events.len() == EVENT_CAPACITY {
395            self.events.pop_front();
396        }
397        self.events.push_back(event);
398    }
399
400    fn send(&mut self, event: Event) {
401        // Sending fails only once every subscription is gone.
402        if let Some(stream) = &self.stream
403            && stream.send(event).is_err()
404        {
405            self.stream = None;
406        }
407    }
408
409    /// A worker's report, which a stopped tunnel no longer takes.
410    fn running(&self) -> bool {
411        self.lifecycle == Lifecycle::Running
412    }
413
414    fn enter(&mut self, phase: RecoveryPhase) {
415        self.phase = phase;
416        let recheck = self.ready
417            && matches!(
418                phase,
419                RecoveryPhase::Capacity
420                    | RecoveryPhase::Resolving
421                    | RecoveryPhase::Connecting
422                    | RecoveryPhase::Handshake
423            );
424        if !recheck && self.announced != phase {
425            self.announced = phase;
426            self.record(TunnelEventKind::PhaseChanged { phase });
427        }
428    }
429
430    fn close_if_drained(&mut self) {
431        if self.lifecycle == Lifecycle::Stopping && self.cleanups == 0 {
432            self.record(TunnelEventKind::Stopped);
433            self.lifecycle = Lifecycle::Closed;
434            self.stream = None;
435        }
436    }
437}
438
439#[derive(Clone)]
440pub(crate) struct Diagnostics(Arc<Mutex<State>>);
441
442impl Default for Diagnostics {
443    fn default() -> Self {
444        Self(Arc::new(Mutex::new(State {
445            lifecycle: Lifecycle::Running,
446            cleanups: 0,
447            stream: None,
448            direct: None,
449            port_mapping: None,
450            phase: RecoveryPhase::Starting,
451            announced: RecoveryPhase::Starting,
452            attempts: 0,
453            failures: 0,
454            failure: None,
455            success: None,
456            latency: None,
457            retry: None,
458            ready: false,
459            network: None,
460            attempt: None,
461            in_flight: 0,
462            seq: 0,
463            events: VecDeque::new(),
464        })))
465    }
466}
467
468impl Diagnostics {
469    fn state(&self) -> std::sync::MutexGuard<'_, State> {
470        self.0.lock().unwrap_or_else(|e| e.into_inner())
471    }
472    /// The history kept so far, then every event from now on, with nothing
473    /// lost or repeated between the two.
474    pub(crate) fn subscribe(&self) -> TunnelEvents {
475        let mut state = self.state();
476        let receiver = match state.lifecycle {
477            // Its sender is gone at once: the history, then the end.
478            Lifecycle::Closed => broadcast::channel(1).1,
479            Lifecycle::Running | Lifecycle::Stopping => state
480                .stream
481                .get_or_insert_with(|| broadcast::channel(STREAM_CAPACITY).0)
482                .subscribe(),
483        };
484        TunnelEvents {
485            replay: state.events.iter().cloned().collect(),
486            receiver,
487        }
488    }
489    /// The tunnel stopped. Later reports from its workers are ignored, and the
490    /// stream ends once every cleanup still running has reported.
491    pub(crate) fn stop(&self) {
492        let mut state = self.state();
493        if state.running() {
494            state.lifecycle = Lifecycle::Stopping;
495            state.phase = RecoveryPhase::Stopped;
496        }
497        state.close_if_drained();
498    }
499    /// Hold the stream open past [`Self::stop`] until the guard drops, so a
500    /// cleanup can say how it went.
501    #[cfg(feature = "port-mapping")]
502    pub(crate) fn defer_close(&self) -> DeferClose {
503        self.state().cleanups += 1;
504        DeferClose(self.clone())
505    }
506    /// This tunnel may map its direct endpoint's port: once that opens, or
507    /// (`Auto`) once punching through has failed.
508    pub(crate) fn port_mapping_requested(
509        &self,
510        mode: crate::sdk::PortMapping,
511        network_generation: u64,
512    ) {
513        let stage = match mode {
514            crate::sdk::PortMapping::Auto => PortMappingStage::Standby,
515            _ => PortMappingStage::WaitingForDirect,
516        };
517        self.state()
518            .port_mapping
519            .get_or_insert_with(|| PortMappingDiagnostics {
520                current: PortMappingStatus::new(stage, network_generation),
521                attempts: 0,
522                renewals: 0,
523                failures: 0,
524            });
525    }
526    #[cfg(feature = "port-mapping")]
527    pub(crate) fn port_mapping_changed(&self, mut status: PortMappingStatus) {
528        if let Some(error) = &mut status.error
529            && let Some((cut, _)) = error.char_indices().nth(256)
530        {
531            error.truncate(cut);
532        }
533        let mut state = self.state();
534        if state.lifecycle == Lifecycle::Closed {
535            return;
536        }
537        let mapping = state
538            .port_mapping
539            .get_or_insert_with(|| PortMappingDiagnostics {
540                current: status.clone(),
541                attempts: 0,
542                renewals: 0,
543                failures: 0,
544            });
545        match status.stage {
546            PortMappingStage::Requesting => mapping.attempts += 1,
547            PortMappingStage::Renewing => mapping.renewals += 1,
548            PortMappingStage::Failed | PortMappingStage::ReleaseFailed => mapping.failures += 1,
549            _ => {}
550        }
551        mapping.current = status.clone();
552        state.record(TunnelEventKind::PortMappingChanged {
553            mapping: Box::new(status),
554        });
555    }
556    pub(crate) fn has_active_direct(&self) -> bool {
557        self.state()
558            .direct
559            .as_ref()
560            .is_some_and(|p| p.active_direct != 0)
561    }
562    #[cfg(feature = "p2p")]
563    pub(crate) fn session_opened(&self, path: SessionPath) {
564        let mut state = self.state();
565        let p = state.direct.get_or_insert_default();
566        match path {
567            SessionPath::Direct => {
568                p.direct_sessions += 1;
569                p.active_direct += 1;
570            }
571            SessionPath::Relay => {
572                p.relay_sessions += 1;
573                p.active_relay += 1;
574            }
575        }
576        state.record(TunnelEventKind::SessionOpened { path });
577    }
578    #[cfg(feature = "p2p")]
579    pub(crate) fn session_closed(&self, path: SessionPath) {
580        let mut state = self.state();
581        let p = state.direct.get_or_insert_default();
582        match path {
583            SessionPath::Direct => p.active_direct = p.active_direct.saturating_sub(1),
584            SessionPath::Relay => p.active_relay = p.active_relay.saturating_sub(1),
585        }
586        state.record(TunnelEventKind::SessionClosed { path });
587    }
588    /// Begin a direct attempt; the guard reports its stages and outcome.
589    #[cfg(feature = "p2p")]
590    pub(crate) fn direct_attempt(&self) -> DirectAttemptGuard {
591        let started = Instant::now();
592        let mut state = self.state();
593        // The attempt rides on the path view, so it shows from the first one.
594        state.direct.get_or_insert_default();
595        state.in_flight = state.in_flight.saturating_add(1);
596        state.attempt = Some((DirectStage::Negotiating, started));
597        state.record(TunnelEventKind::DirectStageChanged {
598            stage: DirectStage::Negotiating,
599        });
600        DirectAttemptGuard {
601            diagnostics: self.clone(),
602            stage: DirectStage::Negotiating,
603            started,
604            settled: false,
605        }
606    }
607
608    pub(crate) fn heard(&self) {
609        self.state().success = Some(Instant::now());
610    }
611    pub(crate) fn responded(&self, elapsed: Duration) {
612        let mut state = self.state();
613        state.success = Some(Instant::now());
614        state.latency = Some(elapsed);
615    }
616    pub(crate) fn phase(&self, phase: RecoveryPhase) {
617        let mut state = self.state();
618        if state.running() {
619            state.enter(phase);
620        }
621    }
622    pub(crate) fn attempt(&self) {
623        let mut state = self.state();
624        if !state.running() {
625            return;
626        }
627        state.attempts = state.attempts.saturating_add(1);
628        state.retry = None;
629        state.enter(RecoveryPhase::Capacity);
630    }
631    pub(crate) fn succeeded(&self, elapsed: Duration) {
632        let mut state = self.state();
633        if !state.running() {
634            return;
635        }
636        state.enter(RecoveryPhase::Ready);
637        state.failures = 0;
638        state.success = Some(Instant::now());
639        state.latency = Some(elapsed);
640        state.retry = None;
641        if !state.ready {
642            state.ready = true;
643            state.record(TunnelEventKind::RelayReady {
644                latency_ms: ms(elapsed),
645            });
646        }
647    }
648    pub(crate) fn failed(&self, failure: RecoveryFailure, retry: Duration) -> bool {
649        let mut state = self.state();
650        if !state.running() {
651            return false;
652        }
653        state.failures = state.failures.saturating_add(1);
654        let emit = state.failure != Some(failure) || state.failures.is_power_of_two();
655        state.enter(RecoveryPhase::Backoff);
656        state.failure = Some(failure);
657        state.retry = Some(Instant::now() + retry);
658        state.ready = false;
659        // A network change is recorded once, by whoever observes it first.
660        if failure != RecoveryFailure::NetworkChanged {
661            state.record(TunnelEventKind::RelayFailed {
662                failure,
663                retry_in_ms: ms(retry),
664            });
665        }
666        emit
667    }
668    /// Note the network generation a worker starts on.
669    pub(crate) fn network_seen(&self, generation: u64) {
670        self.state().network.get_or_insert(generation);
671    }
672    /// Record a change of the local network to `generation`, once however
673    /// many parts of this tunnel observe it.
674    pub(crate) fn network_changed(&self, generation: u64) {
675        let mut state = self.state();
676        if state.network != Some(generation) {
677            state.network = Some(generation);
678            state.record(TunnelEventKind::NetworkChanged);
679        }
680    }
681    pub(crate) fn snapshot(&self, endpoint: &RelayEndpoint) -> TunnelDiagnostics {
682        let state = self.state();
683        let now = Instant::now();
684        let (active_control_setups, active_data_setups) = endpoint.active_setups();
685        let attempt = state.attempt.map(|(stage, started)| DirectAttempt {
686            stage,
687            elapsed_ms: ms(now.saturating_duration_since(started)),
688            in_flight: state.in_flight,
689        });
690        TunnelDiagnostics {
691            relay_transport: endpoint.transport(),
692            direct: state
693                .direct
694                .clone()
695                .map(|direct| PathDiagnostics { attempt, ..direct }),
696            port_mapping: state.port_mapping.clone(),
697            sdk_version: env!("CARGO_PKG_VERSION"),
698            last_attempt_phase: state.phase,
699            attempts: state.attempts,
700            consecutive_failures: state.failures,
701            last_failure: state.failure,
702            last_success_age_ms: state
703                .success
704                .map(|time| ms(now.saturating_duration_since(time))),
705            last_setup_latency_ms: state.latency.map(ms),
706            next_retry_in_ms: state
707                .retry
708                .map(|time| ms(time.saturating_duration_since(now))),
709            dns_age_ms: endpoint.dns_age().map(ms),
710            network_generation: endpoint.network_generation(),
711            active_control_setups,
712            active_data_setups,
713            events: state.events.iter().map(|event| event.public(now)).collect(),
714        }
715    }
716}
717
718pub(crate) fn ms(time: Duration) -> u64 {
719    time.as_millis().min(u128::from(u64::MAX)) as u64
720}
721
722/// Holds a tunnel's event stream open past its stop; see
723/// [`Diagnostics::defer_close`].
724#[cfg(feature = "port-mapping")]
725pub(crate) struct DeferClose(Diagnostics);
726
727#[cfg(feature = "port-mapping")]
728impl Drop for DeferClose {
729    fn drop(&mut self) {
730        let mut state = self.0.state();
731        state.cleanups = state.cleanups.saturating_sub(1);
732        state.close_if_drained();
733    }
734}
735
736/// One direct attempt in flight. Dropped without an outcome (an error after
737/// commit, a cancelled setup), it tells subscribers the attempt was abandoned,
738/// so that one following its stages does not wait on it forever.
739#[cfg(feature = "p2p")]
740pub(crate) struct DirectAttemptGuard {
741    diagnostics: Diagnostics,
742    stage: DirectStage,
743    started: Instant,
744    settled: bool,
745}
746
747#[cfg(feature = "p2p")]
748impl DirectAttemptGuard {
749    pub(crate) fn stage(&mut self, stage: DirectStage) {
750        self.stage = stage;
751        let mut state = self.diagnostics.state();
752        state.attempt = Some((stage, self.started));
753        state.record(TunnelEventKind::DirectStageChanged { stage });
754    }
755    pub(crate) fn established(mut self) {
756        self.settled = true;
757        let setup_ms = ms(self.started.elapsed());
758        self.diagnostics
759            .state()
760            .record(TunnelEventKind::DirectEstablished { setup_ms });
761    }
762    /// Fall back to the relay for `reason`, the legacy label kept in
763    /// [`PathDiagnostics::last_fallback`].
764    pub(crate) fn fell_back(mut self, reason: &str) {
765        self.settled = true;
766        let mut state = self.diagnostics.state();
767        let p = state.direct.get_or_insert_default();
768        p.fallbacks += 1;
769        p.last_fallback = Some(reason.into());
770        state.record(TunnelEventKind::DirectFallback { stage: self.stage });
771    }
772}
773
774#[cfg(feature = "p2p")]
775impl Drop for DirectAttemptGuard {
776    fn drop(&mut self) {
777        let mut state = self.diagnostics.state();
778        state.in_flight = state.in_flight.saturating_sub(1);
779        if state.in_flight == 0 {
780            state.attempt = None;
781        }
782        if !self.settled {
783            state.record(TunnelEventKind::DirectAbandoned { stage: self.stage });
784        }
785    }
786}
787
788/// A session open on a path, closed when this drops.
789#[cfg(feature = "p2p")]
790pub(crate) struct PathUsage(pub Diagnostics, pub SessionPath);
791#[cfg(feature = "p2p")]
792impl Drop for PathUsage {
793    fn drop(&mut self) {
794        self.0.session_closed(self.1);
795    }
796}
797
798#[cfg(test)]
799#[allow(clippy::unwrap_used)]
800mod tests {
801    use super::*;
802
803    fn kinds(diagnostics: &Diagnostics) -> Vec<(TunnelEventKind, u32)> {
804        diagnostics
805            .snapshot(&RelayEndpoint::shared("127.0.0.1:1"))
806            .events
807            .into_iter()
808            .map(|event| (event.kind, event.repeats))
809            .collect()
810    }
811
812    #[tokio::test]
813    async fn history_records_changes_and_folds_repeats() {
814        let diagnostics = Diagnostics::default();
815        diagnostics.succeeded(Duration::from_millis(40));
816        // Steady success is not news: only a change is recorded.
817        diagnostics.succeeded(Duration::from_millis(41));
818        diagnostics.failed(RecoveryFailure::Transport, Duration::from_millis(200));
819        diagnostics.failed(RecoveryFailure::Transport, Duration::from_millis(400));
820        diagnostics.network_changed(1);
821        diagnostics.network_changed(1);
822        diagnostics.failed(RecoveryFailure::NetworkChanged, Duration::ZERO);
823        diagnostics.succeeded(Duration::from_millis(55));
824        assert_eq!(
825            kinds(&diagnostics),
826            vec![
827                (TunnelEventKind::RelayReady { latency_ms: 40 }, 1),
828                (
829                    TunnelEventKind::RelayFailed {
830                        failure: RecoveryFailure::Transport,
831                        retry_in_ms: 400
832                    },
833                    2
834                ),
835                (TunnelEventKind::NetworkChanged, 1),
836                (TunnelEventKind::RelayReady { latency_ms: 55 }, 1),
837            ]
838        );
839    }
840
841    #[tokio::test]
842    async fn history_is_bounded_and_sequenced() {
843        let diagnostics = Diagnostics::default();
844        for generation in 1..=40 {
845            diagnostics.network_changed(generation);
846            diagnostics.failed(RecoveryFailure::Timeout, Duration::ZERO);
847        }
848        let events = diagnostics
849            .snapshot(&RelayEndpoint::shared("127.0.0.1:1"))
850            .events;
851        assert_eq!(events.len(), EVENT_CAPACITY);
852        assert!(events.windows(2).all(|pair| pair[0].seq < pair[1].seq));
853    }
854
855    #[cfg(feature = "p2p")]
856    #[tokio::test]
857    async fn a_direct_attempt_shows_its_stage_until_it_settles() {
858        let diagnostics = Diagnostics::default();
859        let endpoint = RelayEndpoint::shared("127.0.0.1:1");
860        let attempt_of = |d: &Diagnostics| d.snapshot(&endpoint).direct.and_then(|p| p.attempt);
861        let mut events = diagnostics.subscribe();
862        let mut first = diagnostics.direct_attempt();
863        first.stage(DirectStage::Punching);
864        assert_eq!(
865            attempt_of(&diagnostics).unwrap().stage,
866            DirectStage::Punching
867        );
868        let second = diagnostics.direct_attempt();
869        let seen = attempt_of(&diagnostics).unwrap();
870        assert_eq!((seen.stage, seen.in_flight), (DirectStage::Negotiating, 2));
871        first.fell_back("direct_timeout_or_unreachable");
872        assert_eq!(attempt_of(&diagnostics).unwrap().in_flight, 1);
873        second.established();
874        assert_eq!(attempt_of(&diagnostics), None);
875        let direct = diagnostics.snapshot(&endpoint).direct.unwrap();
876        assert_eq!(direct.fallbacks, 1);
877        assert_eq!(
878            direct.last_fallback.as_deref(),
879            Some("direct_timeout_or_unreachable")
880        );
881        // Its stages are steps, delivered live; the history keeps outcomes.
882        assert!(matches!(
883            kinds(&diagnostics)[..],
884            [
885                (
886                    TunnelEventKind::DirectFallback {
887                        stage: DirectStage::Punching
888                    },
889                    1
890                ),
891                (TunnelEventKind::DirectEstablished { .. }, 1),
892            ]
893        ));
894        // One that ends with neither outcome says so, live, and only it does.
895        let mut third = diagnostics.direct_attempt();
896        third.stage(DirectStage::Committing);
897        drop(third);
898        assert_eq!(attempt_of(&diagnostics), None);
899        let abandoned: Vec<_> = std::iter::from_fn(|| events.receiver.try_recv().ok())
900            .map(|event| event.kind)
901            .filter(|kind| matches!(kind, TunnelEventKind::DirectAbandoned { .. }))
902            .collect();
903        assert_eq!(
904            abandoned,
905            [TunnelEventKind::DirectAbandoned {
906                stage: DirectStage::Committing
907            }]
908        );
909    }
910
911    #[tokio::test(start_paused = true)]
912    async fn an_event_is_aged_when_it_is_read() {
913        let diagnostics = Diagnostics::default();
914        let mut live = diagnostics.subscribe();
915        diagnostics.network_changed(1);
916        tokio::time::advance(Duration::from_secs(5)).await;
917        assert_eq!(live.recv().await.unwrap().unwrap().age_ms, 5_000);
918        let mut replayed = diagnostics.subscribe();
919        tokio::time::advance(Duration::from_secs(1)).await;
920        assert_eq!(replayed.recv().await.unwrap().unwrap().age_ms, 6_000);
921    }
922
923    #[tokio::test]
924    async fn subscribers_replay_history_then_follow_every_event_and_learn_of_lag() {
925        let diagnostics = Diagnostics::default();
926        diagnostics.network_changed(1);
927        let mut fast = diagnostics.subscribe();
928        let mut slow = diagnostics.subscribe();
929        assert_eq!(fast.recv().await.unwrap().unwrap().seq, 1);
930        assert_eq!(slow.recv().await.unwrap().unwrap().seq, 1);
931        for generation in 2..=140 {
932            diagnostics.network_changed(generation);
933            let event = fast.recv().await.unwrap().unwrap();
934            assert_eq!((event.seq, event.repeats), (generation, 1));
935        }
936        // Steps reach a subscriber without displacing the history.
937        diagnostics.phase(RecoveryPhase::Connecting);
938        assert_eq!(
939            fast.recv().await.unwrap().unwrap().kind,
940            TunnelEventKind::PhaseChanged {
941                phase: RecoveryPhase::Connecting
942            }
943        );
944        assert_eq!(
945            slow.recv().await.unwrap(),
946            Err(EventsLagged { skipped: 12 })
947        );
948        diagnostics.stop();
949        assert_eq!(
950            fast.recv().await.unwrap().unwrap().kind,
951            TunnelEventKind::Stopped
952        );
953        assert!(fast.recv().await.is_none());
954        let history = kinds(&diagnostics);
955        assert!(history.iter().all(|(kind, _)| !kind.is_live_only()));
956        assert_eq!(history.last().unwrap().0, TunnelEventKind::Stopped);
957    }
958
959    #[tokio::test]
960    async fn a_ready_tunnel_announces_a_failure_but_not_its_routine_rechecks() {
961        let diagnostics = Diagnostics::default();
962        diagnostics.attempt();
963        diagnostics.succeeded(Duration::from_millis(30));
964        let mut events = diagnostics.subscribe();
965        let _ = events.recv().await; // The relay answered.
966        for _ in 0..3 {
967            diagnostics.attempt();
968            diagnostics.phase(RecoveryPhase::Handshake);
969            diagnostics.succeeded(Duration::from_millis(30));
970        }
971        diagnostics.attempt();
972        diagnostics.failed(RecoveryFailure::Timeout, Duration::from_secs(1));
973        diagnostics.attempt();
974        let phases: Vec<_> = std::iter::from_fn(|| events.replay.pop_front())
975            .chain(std::iter::from_fn(|| events.receiver.try_recv().ok()))
976            .filter_map(|event| match event.kind {
977                TunnelEventKind::PhaseChanged { phase } => Some(phase),
978                _ => None,
979            })
980            .collect();
981        assert_eq!(phases, [RecoveryPhase::Backoff, RecoveryPhase::Capacity]);
982    }
983
984    #[cfg(feature = "port-mapping")]
985    #[tokio::test]
986    async fn a_stopped_tunnel_reports_its_mapping_release_before_the_stream_ends() {
987        let diagnostics = Diagnostics::default();
988        let cleanup = diagnostics.defer_close();
989        let mut events = diagnostics.subscribe();
990        diagnostics.stop();
991        diagnostics.port_mapping_changed(PortMappingStatus::new(PortMappingStage::Released, 1));
992        drop(cleanup);
993        assert!(matches!(
994            events.recv().await.unwrap().unwrap().kind,
995            TunnelEventKind::PortMappingChanged { .. }
996        ));
997        assert_eq!(
998            events.recv().await.unwrap().unwrap().kind,
999            TunnelEventKind::Stopped
1000        );
1001        assert!(events.recv().await.is_none());
1002    }
1003}