use crate::endpoint::RelayEndpoint;
use pb_mapper_protocol::direct::SessionPath;
use serde::Serialize;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
use tokio::time::{Duration, Instant};
mod port_mapping;
pub use port_mapping::{
PortMappingDiagnostics, PortMappingProtocol, PortMappingStage, PortMappingStatus,
};
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryPhase {
Starting,
Capacity,
Resolving,
Connecting,
Handshake,
Ready,
Backoff,
Stopped,
}
impl RecoveryPhase {
pub fn as_str(self) -> &'static str {
match self {
Self::Starting => "starting",
Self::Capacity => "capacity",
Self::Resolving => "resolving",
Self::Connecting => "connecting",
Self::Handshake => "handshake",
Self::Ready => "ready",
Self::Backoff => "backoff",
Self::Stopped => "stopped",
}
}
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryFailure {
Dns,
Transport,
Timeout,
ServiceUnavailable,
Rejected,
NetworkChanged,
}
impl RecoveryFailure {
pub fn as_str(self) -> &'static str {
match self {
Self::Dns => "dns",
Self::Transport => "transport",
Self::Timeout => "timeout",
Self::ServiceUnavailable => "service_unavailable",
Self::Rejected => "rejected",
Self::NetworkChanged => "network_changed",
}
}
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DirectStage {
Negotiating,
Discovering,
Punching,
Committing,
}
impl DirectStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Negotiating => "negotiating",
Self::Discovering => "discovering",
Self::Punching => "punching",
Self::Committing => "committing",
}
}
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
pub struct DirectAttempt {
pub stage: DirectStage,
pub elapsed_ms: u64,
pub in_flight: u32,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TunnelEventKind {
RelayReady { latency_ms: u64 },
RelayFailed {
failure: RecoveryFailure,
retry_in_ms: u64,
},
NetworkChanged,
DirectEstablished { setup_ms: u64 },
DirectFallback { stage: DirectStage },
Stopped,
PhaseChanged { phase: RecoveryPhase },
DirectStageChanged { stage: DirectStage },
DirectAbandoned { stage: DirectStage },
SessionOpened { path: SessionPath },
SessionClosed { path: SessionPath },
PortMappingChanged { mapping: Box<PortMappingStatus> },
}
impl TunnelEventKind {
fn folds_into(&self, other: &Self) -> bool {
match (self, other) {
(Self::RelayFailed { failure: a, .. }, Self::RelayFailed { failure: b, .. }) => a == b,
(Self::DirectFallback { stage: a }, Self::DirectFallback { stage: b }) => a == b,
(Self::RelayReady { .. }, Self::RelayReady { .. })
| (Self::NetworkChanged, Self::NetworkChanged)
| (Self::DirectEstablished { .. }, Self::DirectEstablished { .. }) => true,
_ => false,
}
}
fn is_live_only(&self) -> bool {
matches!(
self,
Self::PhaseChanged { .. }
| Self::DirectStageChanged { .. }
| Self::DirectAbandoned { .. }
| Self::SessionOpened { .. }
| Self::SessionClosed { .. }
| Self::PortMappingChanged { .. }
)
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct TunnelEvent {
pub seq: u64,
pub age_ms: u64,
pub repeats: u32,
pub kind: TunnelEventKind,
}
const EVENT_CAPACITY: usize = 16;
const STREAM_CAPACITY: usize = 128;
pub struct TunnelEvents {
replay: VecDeque<Event>,
receiver: broadcast::Receiver<Event>,
}
impl TunnelEvents {
pub async fn recv(&mut self) -> Option<Result<TunnelEvent, EventsLagged>> {
if let Some(event) = self.replay.pop_front() {
return Some(Ok(event.public(Instant::now())));
}
match self.receiver.recv().await {
Ok(event) => Some(Ok(event.public(Instant::now()))),
Err(broadcast::error::RecvError::Lagged(skipped)) => {
Some(Err(EventsLagged { skipped }))
}
Err(broadcast::error::RecvError::Closed) => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventsLagged {
pub skipped: u64,
}
impl std::fmt::Display for EventsLagged {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "missed {} tunnel events", self.skipped)
}
}
impl std::error::Error for EventsLagged {}
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct TunnelDiagnostics {
pub relay_transport: crate::relay::RelayTransport,
#[serde(skip_serializing_if = "Option::is_none")]
pub direct: Option<PathDiagnostics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_mapping: Option<PortMappingDiagnostics>,
pub sdk_version: &'static str,
pub last_attempt_phase: RecoveryPhase,
pub attempts: u64,
pub consecutive_failures: u64,
pub last_failure: Option<RecoveryFailure>,
pub last_success_age_ms: Option<u64>,
pub last_setup_latency_ms: Option<u64>,
pub next_retry_in_ms: Option<u64>,
pub dns_age_ms: Option<u64>,
pub network_generation: u64,
pub active_control_setups: usize,
pub active_data_setups: usize,
pub events: Vec<TunnelEvent>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PathDiagnostics {
pub direct_sessions: u64,
pub relay_sessions: u64,
pub active_direct: u64,
pub active_relay: u64,
pub fallbacks: u64,
pub last_fallback: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attempt: Option<DirectAttempt>,
}
#[derive(Clone)]
struct Event {
seq: u64,
at: Instant,
repeats: u32,
kind: TunnelEventKind,
}
impl Event {
fn public(&self, now: Instant) -> TunnelEvent {
TunnelEvent {
seq: self.seq,
age_ms: ms(now.saturating_duration_since(self.at)),
repeats: self.repeats,
kind: self.kind.clone(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Lifecycle {
Running,
Stopping,
Closed,
}
struct State {
lifecycle: Lifecycle,
cleanups: usize,
stream: Option<broadcast::Sender<Event>>,
direct: Option<PathDiagnostics>,
port_mapping: Option<PortMappingDiagnostics>,
phase: RecoveryPhase,
announced: RecoveryPhase,
attempts: u64,
failures: u64,
failure: Option<RecoveryFailure>,
success: Option<Instant>,
latency: Option<Duration>,
retry: Option<Instant>,
ready: bool,
network: Option<u64>,
attempt: Option<(DirectStage, Instant)>,
in_flight: u32,
seq: u64,
events: VecDeque<Event>,
}
impl State {
fn record(&mut self, kind: TunnelEventKind) {
let live_only = kind.is_live_only();
if self.lifecycle == Lifecycle::Closed || (live_only && self.stream.is_none()) {
return;
}
self.seq = self.seq.wrapping_add(1);
let event = Event {
seq: self.seq,
at: Instant::now(),
repeats: 1,
kind,
};
if live_only {
self.send(event);
return;
}
if self.stream.is_some() {
self.send(event.clone());
}
if let Some(last) = self
.events
.back_mut()
.filter(|last| event.kind.folds_into(&last.kind))
{
*last = Event {
repeats: last.repeats.saturating_add(1),
..event
};
return;
}
if self.events.len() == EVENT_CAPACITY {
self.events.pop_front();
}
self.events.push_back(event);
}
fn send(&mut self, event: Event) {
if let Some(stream) = &self.stream
&& stream.send(event).is_err()
{
self.stream = None;
}
}
fn running(&self) -> bool {
self.lifecycle == Lifecycle::Running
}
fn enter(&mut self, phase: RecoveryPhase) {
self.phase = phase;
let recheck = self.ready
&& matches!(
phase,
RecoveryPhase::Capacity
| RecoveryPhase::Resolving
| RecoveryPhase::Connecting
| RecoveryPhase::Handshake
);
if !recheck && self.announced != phase {
self.announced = phase;
self.record(TunnelEventKind::PhaseChanged { phase });
}
}
fn close_if_drained(&mut self) {
if self.lifecycle == Lifecycle::Stopping && self.cleanups == 0 {
self.record(TunnelEventKind::Stopped);
self.lifecycle = Lifecycle::Closed;
self.stream = None;
}
}
}
#[derive(Clone)]
pub(crate) struct Diagnostics(Arc<Mutex<State>>);
impl Default for Diagnostics {
fn default() -> Self {
Self(Arc::new(Mutex::new(State {
lifecycle: Lifecycle::Running,
cleanups: 0,
stream: None,
direct: None,
port_mapping: None,
phase: RecoveryPhase::Starting,
announced: RecoveryPhase::Starting,
attempts: 0,
failures: 0,
failure: None,
success: None,
latency: None,
retry: None,
ready: false,
network: None,
attempt: None,
in_flight: 0,
seq: 0,
events: VecDeque::new(),
})))
}
}
impl Diagnostics {
fn state(&self) -> std::sync::MutexGuard<'_, State> {
self.0.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn subscribe(&self) -> TunnelEvents {
let mut state = self.state();
let receiver = match state.lifecycle {
Lifecycle::Closed => broadcast::channel(1).1,
Lifecycle::Running | Lifecycle::Stopping => state
.stream
.get_or_insert_with(|| broadcast::channel(STREAM_CAPACITY).0)
.subscribe(),
};
TunnelEvents {
replay: state.events.iter().cloned().collect(),
receiver,
}
}
pub(crate) fn stop(&self) {
let mut state = self.state();
if state.running() {
state.lifecycle = Lifecycle::Stopping;
state.phase = RecoveryPhase::Stopped;
}
state.close_if_drained();
}
#[cfg(feature = "port-mapping")]
pub(crate) fn defer_close(&self) -> DeferClose {
self.state().cleanups += 1;
DeferClose(self.clone())
}
pub(crate) fn port_mapping_requested(
&self,
mode: crate::sdk::PortMapping,
network_generation: u64,
) {
let stage = match mode {
crate::sdk::PortMapping::Auto => PortMappingStage::Standby,
_ => PortMappingStage::WaitingForDirect,
};
self.state()
.port_mapping
.get_or_insert_with(|| PortMappingDiagnostics {
current: PortMappingStatus::new(stage, network_generation),
attempts: 0,
renewals: 0,
failures: 0,
});
}
#[cfg(feature = "port-mapping")]
pub(crate) fn port_mapping_changed(&self, mut status: PortMappingStatus) {
if let Some(error) = &mut status.error
&& let Some((cut, _)) = error.char_indices().nth(256)
{
error.truncate(cut);
}
let mut state = self.state();
if state.lifecycle == Lifecycle::Closed {
return;
}
let mapping = state
.port_mapping
.get_or_insert_with(|| PortMappingDiagnostics {
current: status.clone(),
attempts: 0,
renewals: 0,
failures: 0,
});
match status.stage {
PortMappingStage::Requesting => mapping.attempts += 1,
PortMappingStage::Renewing => mapping.renewals += 1,
PortMappingStage::Failed | PortMappingStage::ReleaseFailed => mapping.failures += 1,
_ => {}
}
mapping.current = status.clone();
state.record(TunnelEventKind::PortMappingChanged {
mapping: Box::new(status),
});
}
pub(crate) fn has_active_direct(&self) -> bool {
self.state()
.direct
.as_ref()
.is_some_and(|p| p.active_direct != 0)
}
#[cfg(feature = "p2p")]
pub(crate) fn session_opened(&self, path: SessionPath) {
let mut state = self.state();
let p = state.direct.get_or_insert_default();
match path {
SessionPath::Direct => {
p.direct_sessions += 1;
p.active_direct += 1;
}
SessionPath::Relay => {
p.relay_sessions += 1;
p.active_relay += 1;
}
}
state.record(TunnelEventKind::SessionOpened { path });
}
#[cfg(feature = "p2p")]
pub(crate) fn session_closed(&self, path: SessionPath) {
let mut state = self.state();
let p = state.direct.get_or_insert_default();
match path {
SessionPath::Direct => p.active_direct = p.active_direct.saturating_sub(1),
SessionPath::Relay => p.active_relay = p.active_relay.saturating_sub(1),
}
state.record(TunnelEventKind::SessionClosed { path });
}
#[cfg(feature = "p2p")]
pub(crate) fn direct_attempt(&self) -> DirectAttemptGuard {
let started = Instant::now();
let mut state = self.state();
state.direct.get_or_insert_default();
state.in_flight = state.in_flight.saturating_add(1);
state.attempt = Some((DirectStage::Negotiating, started));
state.record(TunnelEventKind::DirectStageChanged {
stage: DirectStage::Negotiating,
});
DirectAttemptGuard {
diagnostics: self.clone(),
stage: DirectStage::Negotiating,
started,
settled: false,
}
}
pub(crate) fn heard(&self) {
self.state().success = Some(Instant::now());
}
pub(crate) fn responded(&self, elapsed: Duration) {
let mut state = self.state();
state.success = Some(Instant::now());
state.latency = Some(elapsed);
}
pub(crate) fn phase(&self, phase: RecoveryPhase) {
let mut state = self.state();
if state.running() {
state.enter(phase);
}
}
pub(crate) fn attempt(&self) {
let mut state = self.state();
if !state.running() {
return;
}
state.attempts = state.attempts.saturating_add(1);
state.retry = None;
state.enter(RecoveryPhase::Capacity);
}
pub(crate) fn succeeded(&self, elapsed: Duration) {
let mut state = self.state();
if !state.running() {
return;
}
state.enter(RecoveryPhase::Ready);
state.failures = 0;
state.success = Some(Instant::now());
state.latency = Some(elapsed);
state.retry = None;
if !state.ready {
state.ready = true;
state.record(TunnelEventKind::RelayReady {
latency_ms: ms(elapsed),
});
}
}
pub(crate) fn failed(&self, failure: RecoveryFailure, retry: Duration) -> bool {
let mut state = self.state();
if !state.running() {
return false;
}
state.failures = state.failures.saturating_add(1);
let emit = state.failure != Some(failure) || state.failures.is_power_of_two();
state.enter(RecoveryPhase::Backoff);
state.failure = Some(failure);
state.retry = Some(Instant::now() + retry);
state.ready = false;
if failure != RecoveryFailure::NetworkChanged {
state.record(TunnelEventKind::RelayFailed {
failure,
retry_in_ms: ms(retry),
});
}
emit
}
pub(crate) fn network_seen(&self, generation: u64) {
self.state().network.get_or_insert(generation);
}
pub(crate) fn network_changed(&self, generation: u64) {
let mut state = self.state();
if state.network != Some(generation) {
state.network = Some(generation);
state.record(TunnelEventKind::NetworkChanged);
}
}
pub(crate) fn snapshot(&self, endpoint: &RelayEndpoint) -> TunnelDiagnostics {
let state = self.state();
let now = Instant::now();
let (active_control_setups, active_data_setups) = endpoint.active_setups();
let attempt = state.attempt.map(|(stage, started)| DirectAttempt {
stage,
elapsed_ms: ms(now.saturating_duration_since(started)),
in_flight: state.in_flight,
});
TunnelDiagnostics {
relay_transport: endpoint.transport(),
direct: state
.direct
.clone()
.map(|direct| PathDiagnostics { attempt, ..direct }),
port_mapping: state.port_mapping.clone(),
sdk_version: env!("CARGO_PKG_VERSION"),
last_attempt_phase: state.phase,
attempts: state.attempts,
consecutive_failures: state.failures,
last_failure: state.failure,
last_success_age_ms: state
.success
.map(|time| ms(now.saturating_duration_since(time))),
last_setup_latency_ms: state.latency.map(ms),
next_retry_in_ms: state
.retry
.map(|time| ms(time.saturating_duration_since(now))),
dns_age_ms: endpoint.dns_age().map(ms),
network_generation: endpoint.network_generation(),
active_control_setups,
active_data_setups,
events: state.events.iter().map(|event| event.public(now)).collect(),
}
}
}
pub(crate) fn ms(time: Duration) -> u64 {
time.as_millis().min(u128::from(u64::MAX)) as u64
}
#[cfg(feature = "port-mapping")]
pub(crate) struct DeferClose(Diagnostics);
#[cfg(feature = "port-mapping")]
impl Drop for DeferClose {
fn drop(&mut self) {
let mut state = self.0.state();
state.cleanups = state.cleanups.saturating_sub(1);
state.close_if_drained();
}
}
#[cfg(feature = "p2p")]
pub(crate) struct DirectAttemptGuard {
diagnostics: Diagnostics,
stage: DirectStage,
started: Instant,
settled: bool,
}
#[cfg(feature = "p2p")]
impl DirectAttemptGuard {
pub(crate) fn stage(&mut self, stage: DirectStage) {
self.stage = stage;
let mut state = self.diagnostics.state();
state.attempt = Some((stage, self.started));
state.record(TunnelEventKind::DirectStageChanged { stage });
}
pub(crate) fn established(mut self) {
self.settled = true;
let setup_ms = ms(self.started.elapsed());
self.diagnostics
.state()
.record(TunnelEventKind::DirectEstablished { setup_ms });
}
pub(crate) fn fell_back(mut self, reason: &str) {
self.settled = true;
let mut state = self.diagnostics.state();
let p = state.direct.get_or_insert_default();
p.fallbacks += 1;
p.last_fallback = Some(reason.into());
state.record(TunnelEventKind::DirectFallback { stage: self.stage });
}
}
#[cfg(feature = "p2p")]
impl Drop for DirectAttemptGuard {
fn drop(&mut self) {
let mut state = self.diagnostics.state();
state.in_flight = state.in_flight.saturating_sub(1);
if state.in_flight == 0 {
state.attempt = None;
}
if !self.settled {
state.record(TunnelEventKind::DirectAbandoned { stage: self.stage });
}
}
}
#[cfg(feature = "p2p")]
pub(crate) struct PathUsage(pub Diagnostics, pub SessionPath);
#[cfg(feature = "p2p")]
impl Drop for PathUsage {
fn drop(&mut self) {
self.0.session_closed(self.1);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn kinds(diagnostics: &Diagnostics) -> Vec<(TunnelEventKind, u32)> {
diagnostics
.snapshot(&RelayEndpoint::shared("127.0.0.1:1"))
.events
.into_iter()
.map(|event| (event.kind, event.repeats))
.collect()
}
#[tokio::test]
async fn history_records_changes_and_folds_repeats() {
let diagnostics = Diagnostics::default();
diagnostics.succeeded(Duration::from_millis(40));
diagnostics.succeeded(Duration::from_millis(41));
diagnostics.failed(RecoveryFailure::Transport, Duration::from_millis(200));
diagnostics.failed(RecoveryFailure::Transport, Duration::from_millis(400));
diagnostics.network_changed(1);
diagnostics.network_changed(1);
diagnostics.failed(RecoveryFailure::NetworkChanged, Duration::ZERO);
diagnostics.succeeded(Duration::from_millis(55));
assert_eq!(
kinds(&diagnostics),
vec![
(TunnelEventKind::RelayReady { latency_ms: 40 }, 1),
(
TunnelEventKind::RelayFailed {
failure: RecoveryFailure::Transport,
retry_in_ms: 400
},
2
),
(TunnelEventKind::NetworkChanged, 1),
(TunnelEventKind::RelayReady { latency_ms: 55 }, 1),
]
);
}
#[tokio::test]
async fn history_is_bounded_and_sequenced() {
let diagnostics = Diagnostics::default();
for generation in 1..=40 {
diagnostics.network_changed(generation);
diagnostics.failed(RecoveryFailure::Timeout, Duration::ZERO);
}
let events = diagnostics
.snapshot(&RelayEndpoint::shared("127.0.0.1:1"))
.events;
assert_eq!(events.len(), EVENT_CAPACITY);
assert!(events.windows(2).all(|pair| pair[0].seq < pair[1].seq));
}
#[cfg(feature = "p2p")]
#[tokio::test]
async fn a_direct_attempt_shows_its_stage_until_it_settles() {
let diagnostics = Diagnostics::default();
let endpoint = RelayEndpoint::shared("127.0.0.1:1");
let attempt_of = |d: &Diagnostics| d.snapshot(&endpoint).direct.and_then(|p| p.attempt);
let mut events = diagnostics.subscribe();
let mut first = diagnostics.direct_attempt();
first.stage(DirectStage::Punching);
assert_eq!(
attempt_of(&diagnostics).unwrap().stage,
DirectStage::Punching
);
let second = diagnostics.direct_attempt();
let seen = attempt_of(&diagnostics).unwrap();
assert_eq!((seen.stage, seen.in_flight), (DirectStage::Negotiating, 2));
first.fell_back("direct_timeout_or_unreachable");
assert_eq!(attempt_of(&diagnostics).unwrap().in_flight, 1);
second.established();
assert_eq!(attempt_of(&diagnostics), None);
let direct = diagnostics.snapshot(&endpoint).direct.unwrap();
assert_eq!(direct.fallbacks, 1);
assert_eq!(
direct.last_fallback.as_deref(),
Some("direct_timeout_or_unreachable")
);
assert!(matches!(
kinds(&diagnostics)[..],
[
(
TunnelEventKind::DirectFallback {
stage: DirectStage::Punching
},
1
),
(TunnelEventKind::DirectEstablished { .. }, 1),
]
));
let mut third = diagnostics.direct_attempt();
third.stage(DirectStage::Committing);
drop(third);
assert_eq!(attempt_of(&diagnostics), None);
let abandoned: Vec<_> = std::iter::from_fn(|| events.receiver.try_recv().ok())
.map(|event| event.kind)
.filter(|kind| matches!(kind, TunnelEventKind::DirectAbandoned { .. }))
.collect();
assert_eq!(
abandoned,
[TunnelEventKind::DirectAbandoned {
stage: DirectStage::Committing
}]
);
}
#[tokio::test(start_paused = true)]
async fn an_event_is_aged_when_it_is_read() {
let diagnostics = Diagnostics::default();
let mut live = diagnostics.subscribe();
diagnostics.network_changed(1);
tokio::time::advance(Duration::from_secs(5)).await;
assert_eq!(live.recv().await.unwrap().unwrap().age_ms, 5_000);
let mut replayed = diagnostics.subscribe();
tokio::time::advance(Duration::from_secs(1)).await;
assert_eq!(replayed.recv().await.unwrap().unwrap().age_ms, 6_000);
}
#[tokio::test]
async fn subscribers_replay_history_then_follow_every_event_and_learn_of_lag() {
let diagnostics = Diagnostics::default();
diagnostics.network_changed(1);
let mut fast = diagnostics.subscribe();
let mut slow = diagnostics.subscribe();
assert_eq!(fast.recv().await.unwrap().unwrap().seq, 1);
assert_eq!(slow.recv().await.unwrap().unwrap().seq, 1);
for generation in 2..=140 {
diagnostics.network_changed(generation);
let event = fast.recv().await.unwrap().unwrap();
assert_eq!((event.seq, event.repeats), (generation, 1));
}
diagnostics.phase(RecoveryPhase::Connecting);
assert_eq!(
fast.recv().await.unwrap().unwrap().kind,
TunnelEventKind::PhaseChanged {
phase: RecoveryPhase::Connecting
}
);
assert_eq!(
slow.recv().await.unwrap(),
Err(EventsLagged { skipped: 12 })
);
diagnostics.stop();
assert_eq!(
fast.recv().await.unwrap().unwrap().kind,
TunnelEventKind::Stopped
);
assert!(fast.recv().await.is_none());
let history = kinds(&diagnostics);
assert!(history.iter().all(|(kind, _)| !kind.is_live_only()));
assert_eq!(history.last().unwrap().0, TunnelEventKind::Stopped);
}
#[tokio::test]
async fn a_ready_tunnel_announces_a_failure_but_not_its_routine_rechecks() {
let diagnostics = Diagnostics::default();
diagnostics.attempt();
diagnostics.succeeded(Duration::from_millis(30));
let mut events = diagnostics.subscribe();
let _ = events.recv().await; for _ in 0..3 {
diagnostics.attempt();
diagnostics.phase(RecoveryPhase::Handshake);
diagnostics.succeeded(Duration::from_millis(30));
}
diagnostics.attempt();
diagnostics.failed(RecoveryFailure::Timeout, Duration::from_secs(1));
diagnostics.attempt();
let phases: Vec<_> = std::iter::from_fn(|| events.replay.pop_front())
.chain(std::iter::from_fn(|| events.receiver.try_recv().ok()))
.filter_map(|event| match event.kind {
TunnelEventKind::PhaseChanged { phase } => Some(phase),
_ => None,
})
.collect();
assert_eq!(phases, [RecoveryPhase::Backoff, RecoveryPhase::Capacity]);
}
#[cfg(feature = "port-mapping")]
#[tokio::test]
async fn a_stopped_tunnel_reports_its_mapping_release_before_the_stream_ends() {
let diagnostics = Diagnostics::default();
let cleanup = diagnostics.defer_close();
let mut events = diagnostics.subscribe();
diagnostics.stop();
diagnostics.port_mapping_changed(PortMappingStatus::new(PortMappingStage::Released, 1));
drop(cleanup);
assert!(matches!(
events.recv().await.unwrap().unwrap().kind,
TunnelEventKind::PortMappingChanged { .. }
));
assert_eq!(
events.recv().await.unwrap().unwrap().kind,
TunnelEventKind::Stopped
);
assert!(events.recv().await.is_none());
}
}