Files
irtt-rs/crates/irtt-server/src/clock.rs
T

190 lines
6.2 KiB
Rust

//! Clock sampling for echo timestamps.
//!
//! An echo reply reports when the server received the request and when it sent
//! the answer, so the core needs a clock. It takes one through this private
//! seam rather than reading the system clock inline, so that timestamp behavior
//! is deterministically testable without a runtime, sleeps or timing
//! tolerances.
//!
//! This is the clock counterpart of [`TokenSource`](crate::token::TokenSource)
//! and nothing more. It is **not** a runtime abstraction, not a transport
//! boundary and not a pluggable product API: it is crate-private, the public
//! constructor still takes only a [`ServerConfig`](crate::ServerConfig), and
//! callers never pass timestamps into the core.
use std::{
fmt,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
/// One instant, read from both clock domains together.
///
/// Both fields are signed nanoseconds, matching the wire encoding: `wall_ns`
/// counts from the Unix epoch, and `mono_ns` from an origin the clock source
/// owns. The specification lets a server pick any monotonic origin as long as
/// it is stable for as long as any session it stamps may live, so `mono_ns` is
/// deliberately not process uptime or anything else externally meaningful —
/// only the difference between two samples from the same source has meaning.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ClockSample {
pub(crate) wall_ns: i64,
pub(crate) mono_ns: i64,
}
impl ClockSample {
/// The arithmetic mean of two samples, taken separately per clock domain.
///
/// This is what a midpoint timestamp is: the mean of one reply's receive
/// and send instants. The two domains never mix, and the mean goes through
/// `i128` because `a + b` can overflow `i64` while the mean itself never
/// can.
pub(crate) fn midpoint(self, other: Self) -> Self {
Self {
wall_ns: mean_ns(self.wall_ns, other.wall_ns),
mono_ns: mean_ns(self.mono_ns, other.mono_ns),
}
}
}
/// A source of clock samples.
///
/// Deliberately private, like [`TokenSource`](crate::token::TokenSource): it
/// exists so tests can script the receive and send instants of a reply, not as
/// an extension point. The production implementation is [`SystemClock`].
///
/// Sampling is infallible. A clock that could refuse would make an admitted
/// echo fail for a reason that is neither the peer's fault nor recoverable, so
/// the production source saturates rather than reporting an error.
pub(crate) trait ClockSource: fmt::Debug + Send {
fn sample(&mut self) -> ClockSample;
}
/// Reads the wall clock from [`SystemTime`] and the monotonic clock from an
/// [`Instant`] captured when the source was created.
///
/// That instant is the monotonic origin, so it is fixed for the life of the
/// source and shared by every session the core using it holds — which is
/// exactly the stability the specification asks for.
#[derive(Debug)]
pub(crate) struct SystemClock {
origin: Instant,
}
impl SystemClock {
pub(crate) fn new() -> Self {
Self {
origin: Instant::now(),
}
}
}
impl Default for SystemClock {
fn default() -> Self {
Self::new()
}
}
impl ClockSource for SystemClock {
fn sample(&mut self) -> ClockSample {
ClockSample {
wall_ns: wall_ns(),
mono_ns: saturating_ns(self.origin.elapsed()),
}
}
}
/// Nanoseconds since the Unix epoch, negative before it.
fn wall_ns() -> i64 {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(since_epoch) => saturating_ns(since_epoch),
Err(before_epoch) => saturating_ns(before_epoch.duration()).saturating_neg(),
}
}
/// A duration as nanoseconds, saturating rather than panicking.
///
/// [`Duration`] counts nanoseconds in `u128` and reaches far beyond `i64`,
/// which the wire field is. A host clock set past the year 2262 is a local
/// misconfiguration; it must not become a panic that a remote peer can reach by
/// sending an echo request.
fn saturating_ns(duration: Duration) -> i64 {
i64::try_from(duration.as_nanos()).unwrap_or(i64::MAX)
}
/// The mean of two nanosecond values, computed without overflowing.
fn mean_ns(a: i64, b: i64) -> i64 {
let mean = (i128::from(a) + i128::from(b)) / 2;
// The mean of two `i64` values is always an `i64`, so this conversion
// cannot actually fail. Clamping rather than unwrapping keeps timestamp
// arithmetic free of any panic at all.
i64::try_from(mean).unwrap_or(if mean.is_negative() {
i64::MIN
} else {
i64::MAX
})
}
#[cfg(test)]
mod tests {
use super::*;
/// Constructed values only: the production clock's real readings are not
/// assertable without a timing tolerance, and the behavioral echo tests use
/// a scripted source instead.
#[test]
fn a_midpoint_is_the_per_domain_mean_and_cannot_overflow() {
let receive = ClockSample {
wall_ns: 1_000,
mono_ns: 10_000,
};
let send = ClockSample {
wall_ns: 1_200,
mono_ns: 10_300,
};
assert_eq!(
receive.midpoint(send),
ClockSample {
wall_ns: 1_100,
mono_ns: 10_150,
}
);
// The sum overflows `i64`; the mean does not.
let high = ClockSample {
wall_ns: i64::MAX,
mono_ns: i64::MAX,
};
assert_eq!(
high.midpoint(high),
ClockSample {
wall_ns: i64::MAX,
mono_ns: i64::MAX,
}
);
let low = ClockSample {
wall_ns: i64::MIN,
mono_ns: i64::MIN,
};
assert_eq!(
low.midpoint(low),
ClockSample {
wall_ns: i64::MIN,
mono_ns: i64::MIN,
}
);
assert_eq!(
high.midpoint(low),
ClockSample {
wall_ns: 0,
mono_ns: 0,
}
);
}
#[test]
fn a_duration_beyond_the_wire_field_saturates_rather_than_panicking() {
assert_eq!(saturating_ns(Duration::from_nanos(1_500)), 1_500);
assert_eq!(saturating_ns(Duration::MAX), i64::MAX);
}
}