iroh_relay/
ping_tracker.rs

1use n0_future::time::{self, Duration, Instant};
2use tracing::debug;
3
4/// Maximum time for a ping response in the relay protocol.
5const PING_TIMEOUT: Duration = Duration::from_secs(5);
6
7/// Minimum timeout for an RTT-based health check ping.
8const MIN_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_millis(500);
9
10/// Tracks pings on a single relay connection.
11///
12/// Only the last ping needs is useful, any previously sent ping is forgotten and ignored.
13#[derive(Debug)]
14pub struct PingTracker {
15    inner: Option<PingInner>,
16    max_timeout: Duration,
17    /// Last measured round-trip time to the relay server.
18    last_rtt: Option<Duration>,
19}
20
21#[derive(Debug)]
22struct PingInner {
23    data: [u8; 8],
24    deadline: Instant,
25    sent_at: Instant,
26}
27
28impl Default for PingTracker {
29    fn default() -> Self {
30        Self::new(PING_TIMEOUT)
31    }
32}
33
34impl PingTracker {
35    /// Creates a new ping tracker with the given maximum ping timeout.
36    pub fn new(max_timeout: Duration) -> Self {
37        Self {
38            inner: None,
39            max_timeout,
40            last_rtt: None,
41        }
42    }
43
44    /// Returns the maximum ping timeout.
45    pub fn max_timeout(&self) -> Duration {
46        self.max_timeout
47    }
48
49    /// Starts a new ping with an RTT-based timeout.
50    pub fn new_ping(&mut self) -> [u8; 8] {
51        let timeout = self.ping_timeout();
52        self.new_ping_with_timeout(timeout)
53    }
54
55    /// Starts a new ping with a custom timeout.
56    pub fn new_ping_with_timeout(&mut self, timeout: Duration) -> [u8; 8] {
57        let ping_data = rand::random();
58        let now = Instant::now();
59        debug!(data = ?ping_data, "Sending ping to relay server.");
60        self.inner = Some(PingInner {
61            data: ping_data,
62            deadline: now + timeout,
63            sent_at: now,
64        });
65        ping_data
66    }
67
68    /// Updates the ping tracker with a received pong.
69    ///
70    /// Only the pong of the most recent ping will do anything.  There is no harm feeding
71    /// any pong however.
72    pub fn pong_received(&mut self, data: [u8; 8]) {
73        if let Some(inner) = &self.inner
74            && inner.data == data
75        {
76            let rtt = inner.sent_at.elapsed();
77            debug!(?data, ?rtt, "Pong received from relay server");
78            self.last_rtt = Some(rtt);
79            self.inner = None;
80        }
81    }
82
83    /// Returns the timeout for the next ping.
84    ///
85    /// Uses 3x the last measured RTT (to account for jitter), falling back to
86    /// the default timeout if no RTT has been measured yet.
87    pub fn ping_timeout(&self) -> Duration {
88        self.last_rtt
89            .map(|rtt| (rtt * 3).clamp(MIN_HEALTH_CHECK_TIMEOUT, self.max_timeout))
90            .unwrap_or(self.max_timeout)
91    }
92
93    /// Cancel-safe waiting for a ping timeout.
94    ///
95    /// Unless the most recent sent ping times out, this will never return.
96    pub async fn timeout(&mut self) {
97        match self.inner {
98            Some(PingInner { deadline, data, .. }) => {
99                time::sleep_until(deadline).await;
100                debug!(?data, "Ping timeout.");
101                self.inner = None;
102            }
103            None => std::future::pending().await,
104        }
105    }
106}