use std::time::{Duration, Instant};
use super::StreamId;
pub(super) struct H2DrainState {
draining: bool,
peer_last_stream_id: Option<StreamId>,
started_at: Option<Instant>,
graceful_shutdown_deadline: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum GracefulDrainDecision {
AlreadyDraining,
SendInitial,
}
impl H2DrainState {
pub(super) fn new(graceful_shutdown_deadline: Option<Duration>) -> Self {
let state = H2DrainState {
draining: false,
peer_last_stream_id: None,
started_at: None,
graceful_shutdown_deadline,
};
state.debug_assert_invariants();
state
}
pub(super) fn draining(&self) -> bool {
self.draining
}
pub(super) fn peer_last_stream_id(&self) -> Option<StreamId> {
self.peer_last_stream_id
}
pub(super) fn deadline_elapsed(&self, now: Instant) -> bool {
match (self.started_at, self.graceful_shutdown_deadline) {
(Some(started_at), Some(deadline)) => {
now.saturating_duration_since(started_at) >= deadline
}
_ => false,
}
}
pub(super) fn observe_peer_goaway(&mut self, last_stream_id: StreamId) {
self.draining = true;
self.peer_last_stream_id = Some(last_stream_id);
debug_assert!(self.draining, "observing a peer GOAWAY must mark draining");
debug_assert_eq!(
self.peer_last_stream_id,
Some(last_stream_id),
"observing a peer GOAWAY must record its last_stream_id"
);
self.debug_assert_invariants();
}
pub(super) fn begin_graceful_drain(&mut self, now: Instant) -> GracefulDrainDecision {
if self.draining {
return GracefulDrainDecision::AlreadyDraining;
}
debug_assert!(
self.started_at.is_none(),
"begin_graceful_drain must arm started_at exactly once, on the first call"
);
self.draining = true;
self.started_at = Some(now);
debug_assert!(self.draining, "begin_graceful_drain must mark draining");
debug_assert_eq!(
self.started_at,
Some(now),
"begin_graceful_drain must arm the forced-close budget from its `now` parameter"
);
self.debug_assert_invariants();
GracefulDrainDecision::SendInitial
}
pub(super) fn arm_deadline_if_unarmed(&mut self, now: Instant) {
debug_assert!(
self.draining,
"only a draining connection skips begin_graceful_drain on soft-stop"
);
let started_at = self.started_at;
if started_at.is_none() {
self.started_at = Some(now);
}
debug_assert_eq!(
self.started_at,
Some(started_at.unwrap_or(now)),
"arming must keep an existing budget and arm a missing one from `now`"
);
self.debug_assert_invariants();
}
pub(super) fn enter_final_goaway(&mut self) {
let started_at = self.started_at;
self.draining = true;
debug_assert!(self.draining, "enter_final_goaway must mark draining");
debug_assert_eq!(
self.started_at, started_at,
"enter_final_goaway must not arm or move the forced-close budget"
);
self.debug_assert_invariants();
}
#[cfg(any(test, feature = "e2e-hooks"))]
pub(super) fn __test_started_at(&self) -> Option<Instant> {
self.started_at
}
#[cfg(any(test, feature = "e2e-hooks"))]
pub(super) fn __test_set_draining(&mut self) {
self.draining = true;
}
#[cfg(any(test, feature = "e2e-hooks"))]
pub(super) fn __test_arm_draining(&mut self, started_at: Instant) {
self.draining = true;
self.started_at = Some(started_at);
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
self.started_at.is_none() || self.draining,
"an armed forced-close budget must imply draining"
);
}
#[inline]
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state() {
let drain = H2DrainState::new(Some(Duration::from_secs(5)));
assert!(!drain.draining());
assert_eq!(drain.peer_last_stream_id(), None);
assert_eq!(drain.__test_started_at(), None);
}
#[test]
fn begin_graceful_drain_sends_the_initial_goaway_on_the_first_call() {
let mut drain = H2DrainState::new(None);
let now = Instant::now();
let decision = drain.begin_graceful_drain(now);
assert_eq!(decision, GracefulDrainDecision::SendInitial);
assert!(drain.draining());
assert_eq!(drain.__test_started_at(), Some(now));
}
#[test]
fn begin_graceful_drain_is_idempotent_and_does_not_rearm_the_budget() {
let mut drain = H2DrainState::new(None);
let first = Instant::now();
assert_eq!(
drain.begin_graceful_drain(first),
GracefulDrainDecision::SendInitial
);
let later = first + Duration::from_secs(100);
assert_eq!(
drain.begin_graceful_drain(later),
GracefulDrainDecision::AlreadyDraining
);
assert_eq!(
drain.__test_started_at(),
Some(first),
"a second begin_graceful_drain must not move started_at"
);
}
#[test]
fn deadline_elapsed_false_when_unarmed() {
let drain = H2DrainState::new(Some(Duration::from_secs(5)));
assert!(!drain.deadline_elapsed(Instant::now()));
}
#[test]
fn deadline_elapsed_respects_configured_budget() {
let mut drain = H2DrainState::new(Some(Duration::from_secs(5)));
let armed_at = Instant::now();
drain.begin_graceful_drain(armed_at);
assert!(
!drain.deadline_elapsed(armed_at + Duration::from_secs(5) - Duration::from_millis(1))
);
assert!(drain.deadline_elapsed(armed_at + Duration::from_secs(5)));
}
#[test]
fn deadline_elapsed_never_fires_when_deadline_is_none() {
let mut drain = H2DrainState::new(None);
let armed_at = Instant::now();
drain.begin_graceful_drain(armed_at);
assert!(!drain.deadline_elapsed(armed_at + Duration::from_secs(3600)));
}
#[test]
fn observe_peer_goaway_marks_draining_without_arming_budget() {
let mut drain = H2DrainState::new(Some(Duration::from_secs(5)));
drain.observe_peer_goaway(41);
assert!(drain.draining());
assert_eq!(drain.peer_last_stream_id(), Some(41));
assert_eq!(
drain.__test_started_at(),
None,
"a peer-initiated GOAWAY must not arm the proxy's forced-close budget"
);
}
#[test]
fn soft_stop_after_a_peer_goaway_arms_the_budget_once() {
let mut drain = H2DrainState::new(Some(Duration::from_secs(5)));
drain.observe_peer_goaway(0);
let armed_at = Instant::now();
drain.arm_deadline_if_unarmed(armed_at);
assert_eq!(
drain.__test_started_at(),
Some(armed_at),
"a soft-stop on a connection a peer GOAWAY drained must arm the budget"
);
assert!(!drain.deadline_elapsed(armed_at + Duration::from_secs(4)));
assert!(drain.deadline_elapsed(armed_at + Duration::from_secs(5)));
drain.arm_deadline_if_unarmed(armed_at + Duration::from_secs(3));
assert_eq!(
drain.__test_started_at(),
Some(armed_at),
"a later soft-stop pass must not move or extend the armed budget"
);
}
#[test]
fn enter_final_goaway_after_a_graceful_drain_keeps_the_budget_armed() {
let mut drain = H2DrainState::new(Some(Duration::from_secs(5)));
let armed_at = Instant::now();
drain.begin_graceful_drain(armed_at);
drain.enter_final_goaway();
assert!(drain.draining());
assert_eq!(
drain.__test_started_at(),
Some(armed_at),
"the final GOAWAY must not move the forced-close budget"
);
}
#[test]
fn enter_final_goaway_marks_draining_even_without_a_prior_graceful_drain() {
let mut drain = H2DrainState::new(None);
drain.enter_final_goaway();
assert!(drain.draining());
assert_eq!(drain.__test_started_at(), None);
}
#[test]
fn test_backdoors_match_the_shapes_their_callers_need() {
let mut drain = H2DrainState::new(None);
drain.__test_set_draining();
assert!(drain.draining());
assert_eq!(drain.__test_started_at(), None);
let mut drain = H2DrainState::new(None);
let at = Instant::now();
drain.__test_arm_draining(at);
assert!(drain.draining());
assert_eq!(drain.__test_started_at(), Some(at));
}
}