Skip to main content

reth_eth_wire/
pinger.rs

1use crate::errors::PingerError;
2use std::{
3    future::Future,
4    pin::Pin,
5    task::{Context, Poll, Waker},
6    time::Duration,
7};
8use tokio::time::{Instant, Sleep};
9use tokio_stream::Stream;
10
11/// The pinger is a simple state machine that sends a ping, waits for a pong,
12/// and transitions to timeout if the pong is not received within the timeout.
13#[derive(Debug)]
14pub(crate) struct Pinger {
15    /// The timer used for the next ping.
16    ping_timer: Pin<Box<Sleep>>,
17    /// The last task waker registered with the ping timer.
18    ///
19    /// The pinger is polled on every session poll while its timers only rarely fire, and every
20    /// poll of a tokio timer re-registers with the runtime's timer driver. Caching the registered
21    /// waker allows returning `Pending` with a cheap local check instead, as long as the same
22    /// waker is registered and the timer has not elapsed. Cleared whenever the timer is reset or
23    /// fires so the next poll registers with the new deadline.
24    ping_waker: Option<Waker>,
25    /// The duration between pings.
26    ping_interval: Duration,
27    /// The timer used to detect a ping timeout.
28    timeout_timer: Pin<Box<Sleep>>,
29    /// The last task waker registered with the timeout timer.
30    ///
31    /// See [`Self::ping_waker`] for why the waker is cached.
32    timeout_waker: Option<Waker>,
33    /// The timeout duration for each ping.
34    timeout: Duration,
35    /// Keeps track of the state
36    state: PingState,
37}
38
39// === impl Pinger ===
40
41impl Pinger {
42    /// Creates a new [`Pinger`] with the given ping interval duration,
43    /// and timeout duration.
44    pub(crate) fn new(ping_interval: Duration, timeout_duration: Duration) -> Self {
45        let now = Instant::now();
46        let ping_timer = tokio::time::sleep_until(now + ping_interval);
47        let timeout_timer = tokio::time::sleep(timeout_duration);
48        Self {
49            state: PingState::Ready,
50            ping_timer: Box::pin(ping_timer),
51            ping_waker: None,
52            ping_interval,
53            timeout_timer: Box::pin(timeout_timer),
54            timeout_waker: None,
55            timeout: timeout_duration,
56        }
57    }
58
59    /// Mark a pong as received, and transition the pinger to the `Ready` state if it was in the
60    /// `WaitingForPong` state. Resets readiness by resetting the ping interval.
61    pub(crate) fn on_pong(&mut self) -> Result<(), PingerError> {
62        match self.state {
63            PingState::Ready => Err(PingerError::UnexpectedPong),
64            PingState::WaitingForPong => {
65                self.state = PingState::Ready;
66                self.ping_timer.as_mut().reset(Instant::now() + self.ping_interval);
67                self.ping_waker = None;
68                self.timeout_waker = None;
69                Ok(())
70            }
71            PingState::TimedOut => {
72                // if we receive a pong after timeout then we also reset the state, since the
73                // connection was kept alive after timeout
74                self.state = PingState::Ready;
75                self.ping_timer.as_mut().reset(Instant::now() + self.ping_interval);
76                self.ping_waker = None;
77                self.timeout_waker = None;
78                Ok(())
79            }
80        }
81    }
82
83    /// Returns the current state of the pinger.
84    pub(crate) const fn state(&self) -> PingState {
85        self.state
86    }
87
88    /// Polls the state of the pinger and returns whether a new ping needs to be sent or if a
89    /// previous ping timed out.
90    pub(crate) fn poll_ping(
91        &mut self,
92        cx: &mut Context<'_>,
93    ) -> Poll<Result<PingerEvent, PingerError>> {
94        match self.state() {
95            PingState::Ready => {
96                // Skip polling the timer while it already holds an equivalent waker for a live
97                // deadline; the pending registration is guaranteed to wake this task.
98                if self.ping_waker.as_ref().is_some_and(|waker| waker.will_wake(cx.waker())) &&
99                    !self.ping_timer.is_elapsed()
100                {
101                    return Poll::Pending
102                }
103
104                if self.ping_timer.as_mut().poll(cx).is_ready() {
105                    self.timeout_timer.as_mut().reset(Instant::now() + self.timeout);
106                    self.ping_waker = None;
107                    self.timeout_waker = None;
108                    self.state = PingState::WaitingForPong;
109                    return Poll::Ready(Ok(PingerEvent::Ping))
110                }
111                self.ping_waker = Some(cx.waker().clone());
112            }
113            PingState::WaitingForPong => {
114                // Same skip as above: the timeout timer already holds an equivalent waker.
115                if self.timeout_waker.as_ref().is_some_and(|waker| waker.will_wake(cx.waker())) &&
116                    !self.timeout_timer.is_elapsed()
117                {
118                    return Poll::Pending
119                }
120
121                if self.timeout_timer.as_mut().poll(cx).is_ready() {
122                    self.timeout_waker = None;
123                    self.state = PingState::TimedOut;
124                    return Poll::Ready(Ok(PingerEvent::Timeout))
125                }
126                self.timeout_waker = Some(cx.waker().clone());
127            }
128            PingState::TimedOut => {
129                // we treat continuous calls while in timeout as pending, since the connection is
130                // not yet terminated
131                return Poll::Pending
132            }
133        };
134        Poll::Pending
135    }
136}
137
138impl Stream for Pinger {
139    type Item = Result<PingerEvent, PingerError>;
140
141    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
142        self.get_mut().poll_ping(cx).map(Some)
143    }
144}
145
146/// This represents the possible states of the pinger.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub(crate) enum PingState {
149    /// There are no pings in flight, or all pings have been responded to, and we are ready to send
150    /// a ping at a later point.
151    Ready,
152    /// We have sent a ping and are waiting for a pong, but the peer has missed n pongs.
153    WaitingForPong,
154    /// The peer has failed to respond to a ping.
155    TimedOut,
156}
157
158/// The element type produced by a [`Pinger`], representing either a new
159/// [`Ping`](super::P2PMessage::Ping)
160/// message to send, or an indication that the peer should be timed out.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub(crate) enum PingerEvent {
163    /// A new [`Ping`](super::P2PMessage::Ping) message should be sent.
164    Ping,
165
166    /// The peer should be timed out.
167    Timeout,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use futures::StreamExt;
174    use std::{
175        sync::{
176            atomic::{AtomicUsize, Ordering},
177            Arc,
178        },
179        task::Wake,
180    };
181
182    #[tokio::test]
183    async fn test_poll_ping_with_cached_waker() {
184        struct CountingWaker(AtomicUsize);
185        impl Wake for CountingWaker {
186            fn wake(self: Arc<Self>) {
187                self.0.fetch_add(1, Ordering::Relaxed);
188            }
189        }
190
191        let ping_interval = Duration::from_millis(100);
192        let timeout = Duration::from_millis(100);
193        let slack = Duration::from_millis(50);
194        let mut pinger = Pinger::new(ping_interval, timeout);
195
196        let first = Arc::new(CountingWaker(AtomicUsize::new(0)));
197        let first_waker = Waker::from(Arc::clone(&first));
198        let mut cx = Context::from_waker(&first_waker);
199
200        // repeated polls with the same waker stay pending via the cached-waker fast path
201        for _ in 0..10 {
202            assert!(pinger.poll_ping(&mut cx).is_pending());
203        }
204
205        // the timer still fires and wakes the task exactly once
206        tokio::time::sleep(ping_interval + slack).await;
207        assert_eq!(first.0.load(Ordering::Relaxed), 1);
208        assert!(matches!(pinger.poll_ping(&mut cx), Poll::Ready(Ok(PingerEvent::Ping))));
209        pinger.on_pong().unwrap();
210
211        // a different task waker bypasses the cache and is re-registered with the timer
212        let second = Arc::new(CountingWaker(AtomicUsize::new(0)));
213        let second_waker = Waker::from(Arc::clone(&second));
214        let mut second_cx = Context::from_waker(&second_waker);
215        assert!(pinger.poll_ping(&mut cx).is_pending());
216        assert!(pinger.poll_ping(&mut second_cx).is_pending());
217
218        tokio::time::sleep(ping_interval + slack).await;
219        assert_eq!(first.0.load(Ordering::Relaxed), 1);
220        assert_eq!(second.0.load(Ordering::Relaxed), 1);
221        assert!(matches!(pinger.poll_ping(&mut second_cx), Poll::Ready(Ok(PingerEvent::Ping))));
222
223        // without a pong the timeout timer fires and the pinger reports the timeout
224        assert!(pinger.poll_ping(&mut second_cx).is_pending());
225        tokio::time::sleep(timeout + slack).await;
226        assert_eq!(second.0.load(Ordering::Relaxed), 2);
227        assert!(matches!(pinger.poll_ping(&mut second_cx), Poll::Ready(Ok(PingerEvent::Timeout))));
228    }
229
230    #[tokio::test]
231    async fn test_ping_timeout() {
232        let interval = Duration::from_millis(300);
233        // we should wait for the interval to elapse and receive a pong before the timeout elapses
234        let mut pinger = Pinger::new(interval, Duration::from_millis(20));
235        assert_eq!(pinger.next().await.unwrap().unwrap(), PingerEvent::Ping);
236        pinger.on_pong().unwrap();
237        assert_eq!(pinger.next().await.unwrap().unwrap(), PingerEvent::Ping);
238
239        tokio::time::sleep(interval).await;
240        assert_eq!(pinger.next().await.unwrap().unwrap(), PingerEvent::Timeout);
241        pinger.on_pong().unwrap();
242
243        assert_eq!(pinger.next().await.unwrap().unwrap(), PingerEvent::Ping);
244    }
245}