feat(server): implement echo core
This commit is contained in:
13 files changed
+1426
-144
No files matched your search
@@ -0,0 +1,189 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user