use std::time::{Duration, Instant}; use crate::{error::ClientError, session::AcceptedSessionParameters}; #[derive(Debug)] pub(crate) struct ProbeSchedule { end_at: Option, interval: Duration, next_send_at: Option, } #[derive(Debug)] pub(crate) struct ScheduleCommit { pub(crate) scheduled_at: Instant, pub(crate) next_send_at: Option, } impl ProbeSchedule { pub(crate) fn new( start_at: Instant, accepted: &AcceptedSessionParameters, ) -> Result { let interval = accepted.interval; let end_at = accepted .duration .map(|duration| { start_at .checked_add(duration) .ok_or_else(|| ClientError::NegotiationRejected { reason: "duration is too large to schedule".to_owned(), }) }) .transpose()?; Ok(Self { end_at, interval, next_send_at: Some(start_at), }) } pub(crate) fn next_send_deadline(&self) -> Option { self.next_send_at } /// Wake for finite expiry independently of send readiness until finished. pub(crate) fn end_deadline(&self) -> Option { self.end_at.filter(|_| !self.is_finished()) } pub(crate) fn interval(&self) -> Duration { self.interval } pub(crate) fn permit_probe_at(&mut self, now: Instant) -> bool { if self.next_send_at.is_none() { return false; } if self.end_at.is_some_and(|end| now >= end) { self.next_send_at = None; return false; } true } pub(crate) fn preflight_managed_commit( &self, scheduled_at: Instant, permission_at: Instant, ) -> Result { let (scheduled_at, next_send_at) = advance_cadence(scheduled_at, self.interval, permission_at)?; Ok(ScheduleCommit { scheduled_at, next_send_at: if self.end_at.is_some_and(|end| next_send_at >= end) { None } else { Some(next_send_at) }, }) } pub(crate) fn commit(&mut self, commit: ScheduleCommit) { self.next_send_at = commit.next_send_at; } /// Reclaim slots crossed while a successful socket submission was in flight. /// This follows the send commit; it never advances a failed submission. pub(crate) fn skip_missed_slots_at(&mut self, now: Instant) -> Result<(), ClientError> { if let Some(deadline) = self.next_send_at.filter(|deadline| *deadline <= now) { let commit = self.preflight_managed_commit(deadline, now)?; self.commit(commit); } Ok(()) } pub(crate) fn is_finished(&self) -> bool { self.next_send_at.is_none() } } pub(crate) fn advance_cadence( deadline: Instant, interval: Duration, now: Instant, ) -> Result<(Instant, Instant), ClientError> { if interval.is_zero() { return Err(ClientError::InvalidConfig { reason: "probe interval must be greater than zero".to_owned(), }); } let elapsed_slots = now .checked_duration_since(deadline) .map_or(0, |elapsed| elapsed.as_nanos() / interval.as_nanos()); let scheduled_offset = duration_from_nanos( interval .as_nanos() .checked_mul(elapsed_slots) .ok_or(ClientError::DurationOverflow)?, )?; let next_offset = duration_from_nanos( interval .as_nanos() .checked_mul( elapsed_slots .checked_add(1) .ok_or(ClientError::DurationOverflow)?, ) .ok_or(ClientError::DurationOverflow)?, )?; let scheduled_at = deadline .checked_add(scheduled_offset) .ok_or(ClientError::DurationOverflow)?; let next_at = deadline .checked_add(next_offset) .ok_or(ClientError::DurationOverflow)?; Ok((scheduled_at, next_at)) } fn duration_from_nanos(nanos: u128) -> Result { const NANOS_PER_SECOND: u128 = 1_000_000_000; let seconds = u64::try_from(nanos / NANOS_PER_SECOND).map_err(|_| ClientError::DurationOverflow)?; let subsec_nanos = u32::try_from(nanos % NANOS_PER_SECOND) .expect("nanosecond remainder is always less than one second"); Ok(Duration::new(seconds, subsec_nanos)) } pub(crate) fn instant_abs_diff(left: Instant, right: Instant) -> Duration { left.checked_duration_since(right) .or_else(|| right.checked_duration_since(left)) .unwrap_or(Duration::ZERO) } #[cfg(test)] mod tests { use super::*; #[test] fn lifetime_deadline_exists_only_for_an_unfinished_finite_schedule() { let start = Instant::now(); let end = start + Duration::from_millis(500); let mut accepted = AcceptedSessionParameters { duration: Some(Duration::from_millis(500)), interval: Duration::from_millis(1), length: 0, received_stats: irtt_proto::ReceivedStats::None, stamp_at: irtt_proto::StampAt::None, clock: irtt_proto::Clock::Both, dscp: 0, server_fill: None, }; let mut finite = ProbeSchedule::new(start, &accepted).unwrap(); assert_eq!(finite.end_deadline(), Some(end)); assert!(finite.permit_probe_at(end - Duration::from_nanos(1))); assert_eq!(finite.end_deadline(), Some(end)); assert!(!finite.permit_probe_at(end)); assert!(finite.is_finished()); assert_eq!(finite.end_deadline(), None); let mut finite = ProbeSchedule::new(start, &accepted).unwrap(); let commit = finite .preflight_managed_commit(start, end - Duration::from_nanos(1)) .unwrap(); finite.commit(commit); assert!(finite.is_finished()); assert_eq!(finite.end_deadline(), None); accepted.duration = None; let mut continuous = ProbeSchedule::new(start, &accepted).unwrap(); assert!(continuous.permit_probe_at(end)); assert_eq!(continuous.end_deadline(), None); } }