use std::time::Duration;
use crate::{Backoff, DEFAULT_CONNECT_TIMEOUT};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Timing {
pub ping_interval: Option<Duration>,
pub stale_after: Duration,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub connect_timeout: Duration,
pub max_connection_age: Option<Duration>,
}
impl Default for Timing {
fn default() -> Self {
Self {
ping_interval: Some(Duration::from_secs(20)),
stale_after: Duration::from_secs(30),
initial_backoff: Duration::from_millis(500),
max_backoff: Duration::from_secs(60),
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
max_connection_age: None,
}
}
}
impl Timing {
pub fn backoff(&self) -> Backoff {
Backoff::new(self.initial_backoff, self.max_backoff)
}
pub fn next_wait(&self, since_ping: Duration, silent: Duration, age: Duration) -> Duration {
next_wait([
(self.ping_interval, since_ping),
(Some(self.stale_after), silent),
(self.max_connection_age, age),
])
}
}
pub fn next_wait(deadlines: impl IntoIterator<Item = (Option<Duration>, Duration)>) -> Duration {
deadlines
.into_iter()
.filter_map(|(limit, elapsed)| limit.map(|limit| limit.saturating_sub(elapsed)))
.min()
.unwrap_or(Duration::MAX)
}
#[macro_export]
macro_rules! timing_setters {
() => {};
($(#[$meta:meta])* ping_interval; $($rest:tt)*) => {
$(#[$meta])*
pub fn ping_interval(mut self, interval: ::core::time::Duration) -> Self {
self.timing.ping_interval = ::core::option::Option::Some(interval);
self
}
$crate::timing_setters! { $($rest)* }
};
($(#[$meta:meta])* stale_after; $($rest:tt)*) => {
$(#[$meta])*
pub fn stale_after(mut self, stale_after: ::core::time::Duration) -> Self {
self.timing.stale_after = stale_after;
self
}
$crate::timing_setters! { $($rest)* }
};
($(#[$meta:meta])* backoff; $($rest:tt)*) => {
$(#[$meta])*
pub fn backoff(
mut self,
initial: ::core::time::Duration,
max: ::core::time::Duration,
) -> Self {
self.timing.initial_backoff = initial;
self.timing.max_backoff = max;
self
}
$crate::timing_setters! { $($rest)* }
};
($(#[$meta:meta])* connect_timeout; $($rest:tt)*) => {
$(#[$meta])*
pub fn connect_timeout(mut self, timeout: ::core::time::Duration) -> Self {
self.timing.connect_timeout = timeout;
self
}
$crate::timing_setters! { $($rest)* }
};
($(#[$meta:meta])* max_connection_age; $($rest:tt)*) => {
$(#[$meta])*
pub fn max_connection_age(mut self, age: ::core::time::Duration) -> Self {
self.timing.max_connection_age = ::core::option::Option::Some(age);
self
}
$crate::timing_setters! { $($rest)* }
};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default)]
struct Builder {
timing: Timing,
}
impl Builder {
crate::timing_setters! {
ping_interval;
stale_after;
backoff;
connect_timeout;
max_connection_age;
}
}
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
#[test]
fn the_defaults_are_the_shared_cadences() {
let timing = Timing::default();
assert_eq!(timing.ping_interval, Some(secs(20)));
assert_eq!(timing.stale_after, secs(30));
assert_eq!(timing.initial_backoff, Duration::from_millis(500));
assert_eq!(timing.max_backoff, secs(60));
assert_eq!(timing.connect_timeout, secs(10));
assert_eq!(timing.max_connection_age, None);
}
#[test]
fn each_setter_writes_its_own_field() {
let builder = Builder::default()
.ping_interval(secs(1))
.stale_after(secs(2))
.backoff(Duration::from_millis(3), Duration::from_millis(4))
.connect_timeout(secs(5))
.max_connection_age(secs(6));
assert_eq!(
builder.timing,
Timing {
ping_interval: Some(secs(1)),
stale_after: secs(2),
initial_backoff: Duration::from_millis(3),
max_backoff: Duration::from_millis(4),
connect_timeout: secs(5),
max_connection_age: Some(secs(6)),
}
);
assert_eq!(builder.timing.backoff().take(), Duration::from_millis(3));
}
#[test]
fn with_no_ping_and_no_age_the_wait_is_the_staleness_window_alone() {
let timing = Timing {
ping_interval: None,
max_connection_age: None,
..Timing::default()
};
assert_eq!(timing.next_wait(secs(999), secs(12), secs(999)), secs(18));
}
#[test]
fn the_nearest_deadline_wins() {
let timing = Timing {
max_connection_age: Some(secs(100)),
..Timing::default()
};
assert_eq!(timing.next_wait(secs(15), secs(20), secs(99)), secs(1));
assert_eq!(timing.next_wait(secs(15), secs(20), secs(0)), secs(5));
assert_eq!(timing.next_wait(secs(0), secs(25), secs(0)), secs(5));
}
#[test]
fn no_deadline_at_all_waits_for_ever() {
assert_eq!(next_wait([]), Duration::MAX);
assert_eq!(
next_wait([(None, secs(1)), (None, secs(2)), (None, Duration::MAX)]),
Duration::MAX
);
}
#[test]
fn a_deadline_already_past_waits_zero() {
assert_eq!(next_wait([(Some(secs(30)), secs(31))]), Duration::ZERO);
assert_eq!(
next_wait([(Some(secs(30)), Duration::MAX), (None, secs(0))]),
Duration::ZERO
);
}
#[test]
fn a_limit_of_duration_max_saturates_rather_than_overflowing() {
assert_eq!(
next_wait([(Some(Duration::MAX), secs(1))]),
Duration::MAX - secs(1)
);
let never = Timing {
ping_interval: Some(Duration::MAX),
stale_after: Duration::MAX,
..Timing::default()
};
assert_eq!(
never.next_wait(secs(1), secs(1), secs(1)),
Duration::MAX - secs(1)
);
}
}