use std::time::Duration;
use tokio::time::{Instant, sleep_until};
use tracing::warn;
#[derive(Debug)]
pub struct PingManager {
pending: Option<Instant>,
timeout: Duration,
}
impl PingManager {
pub fn new(timeout_secs: u64) -> Self {
Self {
pending: None,
timeout: Duration::from_secs(timeout_secs),
}
}
pub fn sent(&mut self) {
if self.pending.replace(Instant::now()).is_some() {
warn!("Sent new ping before receiving pong for previous ping");
}
}
pub fn received(&mut self) {
self.pending = None;
}
pub async fn timed_out(&mut self) {
match self.pending {
Some(sent_at) => {
sleep_until(sent_at + self.timeout).await;
self.pending = None;
}
None => std::future::pending().await,
}
}
#[cfg(test)]
pub fn next_timeout_at(&self) -> Option<Instant> {
self.pending.map(|sent_at| sent_at + self.timeout)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn new_has_no_pending() {
let mgr = PingManager::new(60);
assert!(mgr.next_timeout_at().is_none());
}
#[test]
fn sent_marks_pending() {
let mut mgr = PingManager::new(60);
mgr.sent();
assert!(mgr.next_timeout_at().is_some());
}
#[test]
fn received_clears_pending() {
let mut mgr = PingManager::new(60);
mgr.sent();
mgr.received();
assert!(mgr.next_timeout_at().is_none());
}
#[tokio::test(start_paused = true)]
async fn timed_out_waits_for_the_timeout() {
let mut mgr = PingManager::new(60);
mgr.sent();
let started = Instant::now();
mgr.timed_out().await;
assert_eq!(Instant::now() - started, Duration::from_secs(60));
}
#[tokio::test(start_paused = true)]
async fn timed_out_never_resolves_without_a_pending_ping() {
let mut mgr = PingManager::new(60);
tokio::select! {
_ = mgr.timed_out() => panic!("resolved with no ping pending"),
_ = tokio::time::sleep(Duration::from_secs(3600)) => {}
}
}
#[test]
fn sent_twice_replaces_pending() {
let mut mgr = PingManager::new(60);
mgr.sent();
mgr.sent(); assert!(mgr.next_timeout_at().is_some());
}
#[tokio::test(start_paused = true)]
async fn timed_out_clears_pending_so_it_reports_once() {
let mut mgr = PingManager::new(1);
mgr.sent();
mgr.timed_out().await;
assert!(mgr.next_timeout_at().is_none());
tokio::select! {
_ = mgr.timed_out() => panic!("reported the same ping twice"),
_ = tokio::time::sleep(Duration::from_secs(3600)) => {}
}
}
}