1use 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#[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 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#[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 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#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
81#[serde(rename_all = "snake_case")]
82pub enum DirectStage {
83 Negotiating,
86 Discovering,
88 Punching,
90 Committing,
92}
93
94impl DirectStage {
95 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#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
108pub struct DirectAttempt {
109 pub stage: DirectStage,
111 pub elapsed_ms: u64,
113 pub in_flight: u32,
115}
116
117#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
124#[serde(tag = "kind", rename_all = "snake_case")]
125pub enum TunnelEventKind {
126 RelayReady { latency_ms: u64 },
128 RelayFailed {
131 failure: RecoveryFailure,
132 retry_in_ms: u64,
133 },
134 NetworkChanged,
136 DirectEstablished { setup_ms: u64 },
138 DirectFallback { stage: DirectStage },
140 Stopped,
143 PhaseChanged { phase: RecoveryPhase },
147 DirectStageChanged { stage: DirectStage },
149 DirectAbandoned { stage: DirectStage },
153 SessionOpened { path: SessionPath },
155 SessionClosed { path: SessionPath },
157 PortMappingChanged { mapping: Box<PortMappingStatus> },
159}
160
161impl TunnelEventKind {
162 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 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#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
192pub struct TunnelEvent {
193 pub seq: u64,
196 pub age_ms: u64,
198 pub repeats: u32,
200 pub kind: TunnelEventKind,
202}
203
204const EVENT_CAPACITY: usize = 16;
206
207const STREAM_CAPACITY: usize = 128;
209
210pub struct TunnelEvents {
219 replay: VecDeque<Event>,
220 receiver: broadcast::Receiver<Event>,
221}
222
223impl TunnelEvents {
224 pub async fn recv(&mut self) -> Option<Result<TunnelEvent, EventsLagged>> {
228 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#[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#[derive(Clone, Debug, Serialize)]
259#[non_exhaustive]
260pub struct TunnelDiagnostics {
261 pub relay_transport: crate::relay::RelayTransport,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub direct: Option<PathDiagnostics>,
265 #[serde(skip_serializing_if = "Option::is_none")]
268 pub port_mapping: Option<PortMappingDiagnostics>,
269 pub sdk_version: &'static str,
270 pub last_attempt_phase: RecoveryPhase,
272 pub attempts: u64,
273 pub consecutive_failures: u64,
274 pub last_failure: Option<RecoveryFailure>,
275 pub last_success_age_ms: Option<u64>,
277 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 pub active_control_setups: usize,
284 pub active_data_setups: usize,
285 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 #[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#[derive(Clone, Copy, PartialEq, Eq)]
324enum Lifecycle {
325 Running,
326 Stopping,
328 Closed,
330}
331
332struct State {
333 lifecycle: Lifecycle,
334 cleanups: usize,
336 stream: Option<broadcast::Sender<Event>>,
339 direct: Option<PathDiagnostics>,
340 port_mapping: Option<PortMappingDiagnostics>,
341 phase: RecoveryPhase,
342 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 ready: bool,
353 network: Option<u64>,
356 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 if let Some(stream) = &self.stream
403 && stream.send(event).is_err()
404 {
405 self.stream = None;
406 }
407 }
408
409 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 pub(crate) fn subscribe(&self) -> TunnelEvents {
475 let mut state = self.state();
476 let receiver = match state.lifecycle {
477 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 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 #[cfg(feature = "port-mapping")]
502 pub(crate) fn defer_close(&self) -> DeferClose {
503 self.state().cleanups += 1;
504 DeferClose(self.clone())
505 }
506 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 #[cfg(feature = "p2p")]
590 pub(crate) fn direct_attempt(&self) -> DirectAttemptGuard {
591 let started = Instant::now();
592 let mut state = self.state();
593 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 if failure != RecoveryFailure::NetworkChanged {
661 state.record(TunnelEventKind::RelayFailed {
662 failure,
663 retry_in_ms: ms(retry),
664 });
665 }
666 emit
667 }
668 pub(crate) fn network_seen(&self, generation: u64) {
670 self.state().network.get_or_insert(generation);
671 }
672 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#[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#[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 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#[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 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 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 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 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; 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}