Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 34s
Rust CI / cargo clippy (push) Successful in 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 23s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m11s
159 lines
6.7 KiB
Rust
159 lines
6.7 KiB
Rust
//! Clock sampling for echo timestamps.
|
|
//!
|
|
//! The public [`ServerCore::handle_datagram`](crate::ServerCore::handle_datagram)
|
|
//! entry point takes no timestamp: the core samples every instant it reports.
|
|
//! The Tokio runtime uses a crate-private entry point that may additionally hand
|
|
//! over a kernel-observed wall receive time for one datagram, which this module
|
|
//! contributes only the [`wall_ns_of`] conversion for. Whether such an
|
|
//! observation is usable, and which reply field it may reach, is measurement
|
|
//! policy and lives in the core — this stays the module that reads clocks, not
|
|
//! one that models transport metadata. A [`ClockSample`] remains one paired
|
|
//! userspace instant and never carries a kernel reading.
|
|
|
|
use std::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),
|
|
}
|
|
}
|
|
|
|
/// This sample, held back in any domain where it runs ahead of `later`.
|
|
///
|
|
/// A reply's receive instant must not be later than its send instant. The
|
|
/// monotonic domain gets that from its source, which only moves forward.
|
|
/// The wall clock does not: it can be stepped backwards between two
|
|
/// readings by NTP, a hypervisor or an administrator, which would otherwise
|
|
/// invert the pair.
|
|
///
|
|
/// The *earlier* reading is the one that moves, so the pair settles on the
|
|
/// clock as it now stands rather than on a value the clock has already
|
|
/// disowned. Nothing is remembered between calls: a latch that carried a
|
|
/// pre-step value forward would keep reporting a wall time the host has
|
|
/// corrected away, which is the smoothing across packets the specification
|
|
/// forbids — and it would make one-way delays wrong for as long as the
|
|
/// latch held, rather than for one reply.
|
|
pub(crate) fn not_after(self, later: Self) -> Self {
|
|
Self {
|
|
wall_ns: self.wall_ns.min(later.wall_ns),
|
|
mono_ns: self.mono_ns.min(later.mono_ns),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 SystemClock {
|
|
pub(crate) 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(),
|
|
}
|
|
}
|
|
|
|
/// An observed instant as nanoseconds since the Unix epoch, or `None` when it
|
|
/// does not fit the wire's signed-nanosecond field.
|
|
///
|
|
/// This is the conversion for an instant the server did **not** read from its
|
|
/// own clock — today, a kernel receive timestamp the transport
|
|
/// observed. It reports unrepresentability rather than saturating, because a
|
|
/// clamped instant is indistinguishable from a real one at the boundary and
|
|
/// would then be compared against a genuine sample as though it were plausible.
|
|
/// A local reading has no such problem and keeps [`saturating_ns`].
|
|
///
|
|
/// The negation cannot fail for any [`SystemTime`] a host can hold: it is
|
|
/// reachable only from a value already inside `i64`, and only `i64::MIN` has no
|
|
/// negation. It is checked anyway so that no path here can panic.
|
|
pub(crate) fn wall_ns_of(at: SystemTime) -> Option<i64> {
|
|
match at.duration_since(UNIX_EPOCH) {
|
|
Ok(since_epoch) => i64::try_from(since_epoch.as_nanos()).ok(),
|
|
Err(before_epoch) => i64::try_from(before_epoch.duration().as_nanos())
|
|
.ok()
|
|
.and_then(i64::checked_neg),
|
|
}
|
|
}
|
|
|
|
/// A duration as nanoseconds, saturating rather than panicking.
|
|
///
|
|
/// This is the crate's one [`Duration`] → wire-nanoseconds conversion, used for
|
|
/// clock readings and for the configured intervals and deadlines that
|
|
/// negotiation and lifetime policy compare against them. Everything on both
|
|
/// sides is signed nanoseconds, so there is no second rule anywhere.
|
|
///
|
|
/// [`Duration`] counts nanoseconds in `u128` and reaches far beyond `i64`,
|
|
/// which the wire field is. A host clock set past the year 2262, or an operator
|
|
/// configuring a millennium-long idle timeout, is a local matter; neither may
|
|
/// become a panic that a remote peer can reach by sending a packet. Saturating
|
|
/// loses nothing that could have been honored, since no wire interval or
|
|
/// duration can express more than `i64::MAX` nanoseconds either.
|
|
pub(crate) 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
|
|
})
|
|
}
|