use std::time::Instant;
use crate::constants;
use super::frame::Close;
pub(crate) enum Lifecycle {
Live,
Closing(Closing),
Draining {
until: Instant,
},
Dead,
}
impl Lifecycle {
pub(crate) fn is_live(&self) -> bool {
matches!(self, Lifecycle::Live)
}
pub(crate) fn is_dead(&self) -> bool {
matches!(self, Lifecycle::Dead)
}
pub(crate) fn linger_until(&self) -> Option<Instant> {
match self {
Lifecycle::Live | Lifecycle::Dead => None,
Lifecycle::Closing(closing) => Some(closing.until),
Lifecycle::Draining { until } => Some(*until),
}
}
}
pub(crate) struct Closing {
until: Instant,
close: Close,
last_reply: Option<Instant>,
}
impl Closing {
pub(crate) fn new(now: Instant, close: Close) -> Self {
Self {
until: now + constants::CLOSE_LINGER,
close,
last_reply: None,
}
}
pub(crate) fn until(&self) -> Instant {
self.until
}
pub(crate) fn reply(&mut self, now: Instant) -> Option<Close> {
let due = match self.last_reply {
None => true,
Some(last) => now.duration_since(last) >= constants::CLOSE_REPLY_MIN_INTERVAL,
};
if !due {
return None;
}
self.last_reply = Some(now);
Some(self.close.clone())
}
pub(crate) fn into_draining(self) -> Lifecycle {
Lifecycle::Draining { until: self.until }
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn close_frame() -> Close {
Close::new(constants::NO_ERROR, b"bye")
}
#[test]
fn closing_lingers_for_close_linger() {
let now = Instant::now();
let closing = Closing::new(now, close_frame());
assert_eq!(closing.until(), now + constants::CLOSE_LINGER);
}
#[test]
fn the_first_reply_is_not_rate_limited_by_the_opening_close() {
let now = Instant::now();
let mut closing = Closing::new(now, close_frame());
assert_eq!(closing.reply(now), Some(close_frame()));
}
#[test]
fn replies_are_capped_at_one_per_second() {
let now = Instant::now();
let mut closing = Closing::new(now, close_frame());
let mut replies = 0;
for step in 0..10 {
if closing
.reply(now + Duration::from_millis(step * 90))
.is_some()
{
replies += 1;
}
}
assert_eq!(replies, 1, "ten packets inside one second, one reply");
assert!(
closing
.reply(now + constants::CLOSE_REPLY_MIN_INTERVAL - Duration::from_millis(1))
.is_none()
);
assert!(
closing
.reply(now + constants::CLOSE_REPLY_MIN_INTERVAL)
.is_some()
);
}
#[test]
fn a_reply_is_the_original_close() {
let now = Instant::now();
let close = Close::new(0x2a, b"a reason");
let mut closing = Closing::new(now, close.clone());
assert_eq!(closing.reply(now), Some(close.clone()));
assert_eq!(
closing.reply(now + constants::CLOSE_REPLY_MIN_INTERVAL),
Some(close)
);
}
#[test]
fn closing_to_draining_keeps_the_original_deadline() {
let now = Instant::now();
let closing = Closing::new(now, close_frame());
let expiry = closing.until();
let draining = closing.into_draining();
assert_eq!(draining.linger_until(), Some(expiry));
assert!(!draining.is_live());
}
#[test]
fn live_and_dead_have_no_linger() {
assert_eq!(Lifecycle::Live.linger_until(), None);
assert!(Lifecycle::Live.is_live());
assert_eq!(Lifecycle::Dead.linger_until(), None);
assert!(Lifecycle::Dead.is_dead());
}
}