Skip to main content

dash_runtime/timing/
clock.rs

1//! The UTC clock: an anchor pair mapping the caller's monotonic microseconds
2//! to UTC, with a trust-ordered replacement rule.
3//!
4//! `docs/dash-runtime/DESIGN.md` 3.2/3.3. Pure: the clock never reads a time
5//! source, it only maps instants it is given.
6
7use broadcast_common::impl_spec_display;
8
9use super::utc_timing::UtcSample;
10
11/// Where an anchor came from, ordered by trust (lowest first).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[non_exhaustive]
14pub enum ClockSource {
15    /// The `Date` of the MPD response.
16    MpdDate,
17    /// The host's own wall clock.
18    Host,
19    /// A `UTCTiming` measurement.
20    UtcTiming,
21}
22
23impl ClockSource {
24    /// Stable snake_case label.
25    #[must_use]
26    pub fn name(&self) -> &'static str {
27        match self {
28            Self::MpdDate => "mpd_date",
29            Self::Host => "host",
30            Self::UtcTiming => "utc_timing",
31        }
32    }
33}
34impl_spec_display!(ClockSource);
35
36/// One anchor: at monotonic `mono_us` UTC was `utc_us`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct Anchor {
40    /// Monotonic microseconds.
41    pub mono_us: u64,
42    /// Unix microseconds.
43    pub utc_us: i64,
44    /// Where it came from.
45    pub source: ClockSource,
46    /// Half-width of the error bar.
47    pub uncertainty_us: u64,
48}
49
50impl Anchor {
51    /// Anchor from a measured sample.
52    #[must_use]
53    pub fn from_sample(sample: UtcSample, source: ClockSource) -> Self {
54        Self {
55            mono_us: sample.mono_us,
56            utc_us: sample.utc_us,
57            source,
58            uncertainty_us: sample.uncertainty_us,
59        }
60    }
61
62    /// UTC at `now_us`, saturating at the `i64` range.
63    #[must_use]
64    pub fn utc_at(&self, now_us: u64) -> i64 {
65        let delta = i128::from(now_us) - i128::from(self.mono_us);
66        saturate(i128::from(self.utc_us) + delta)
67    }
68
69    /// The monotonic instant at which UTC reaches `utc_us`, saturating at
70    /// `0..=u64::MAX`.
71    #[must_use]
72    pub fn mono_for_utc(&self, utc_us: i64) -> u64 {
73        let delta = i128::from(utc_us) - i128::from(self.utc_us);
74        let mono = i128::from(self.mono_us) + delta;
75        u64::try_from(mono.clamp(0, i128::from(u64::MAX))).unwrap_or(u64::MAX)
76    }
77}
78
79fn saturate(v: i128) -> i64 {
80    i64::try_from(v.clamp(i128::from(i64::MIN), i128::from(i64::MAX))).unwrap_or(i64::MAX)
81}
82
83/// Why an offered sample replaced the anchor.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum AdoptReason {
87    /// There was no anchor.
88    First,
89    /// The source is more trusted than the current one.
90    HigherTrust,
91    /// Same source, at least as precise.
92    Refined,
93    /// Same source, the old anchor was older than the resync period.
94    Stale,
95    /// Same source, the time disagrees with the prediction by more than both
96    /// error bars: a clock step.
97    Step,
98    /// A less trusted source, allowed because the better one failed.
99    Downgrade,
100}
101
102impl AdoptReason {
103    /// Stable snake_case label.
104    #[must_use]
105    pub fn name(&self) -> &'static str {
106        match self {
107            Self::First => "first",
108            Self::HigherTrust => "higher_trust",
109            Self::Refined => "refined",
110            Self::Stale => "stale",
111            Self::Step => "step",
112            Self::Downgrade => "downgrade",
113        }
114    }
115}
116impl_spec_display!(AdoptReason);
117
118/// Why an offered sample was not adopted.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[non_exhaustive]
121pub enum RejectReason {
122    /// A more trusted anchor exists.
123    LowerTrust,
124    /// Same source but less precise and not stale.
125    LessPrecise,
126}
127
128impl RejectReason {
129    /// Stable snake_case label.
130    #[must_use]
131    pub fn name(&self) -> &'static str {
132        match self {
133            Self::LowerTrust => "lower_trust",
134            Self::LessPrecise => "less_precise",
135        }
136    }
137}
138impl_spec_display!(RejectReason);
139
140/// The result of [`UtcClock::offer`].
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum OfferOutcome {
144    /// The sample is now the anchor.
145    Adopted(AdoptReason),
146    /// The existing anchor was kept.
147    Rejected(RejectReason),
148}
149
150/// Parameters of the replacement rule.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct OfferPolicy {
154    /// An anchor older than this is replaced by any same-source sample.
155    pub resync_us: u64,
156    /// Let a less trusted source replace a more trusted one (the better one
157    /// has failed for the configured wait).
158    pub allow_downgrade: bool,
159}
160
161impl OfferPolicy {
162    /// A policy.
163    #[must_use]
164    pub fn new(resync_us: u64, allow_downgrade: bool) -> Self {
165        Self {
166            resync_us,
167            allow_downgrade,
168        }
169    }
170}
171
172/// The clock: at most one anchor.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub struct UtcClock {
175    anchor: Option<Anchor>,
176}
177
178impl UtcClock {
179    /// A clock with no anchor.
180    #[must_use]
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// The current anchor.
186    #[must_use]
187    pub fn anchor(&self) -> Option<&Anchor> {
188        self.anchor.as_ref()
189    }
190
191    /// UTC at `now_us`, or `None` before the first anchor.
192    #[must_use]
193    pub fn utc_at(&self, now_us: u64) -> Option<i64> {
194        self.anchor.map(|a| a.utc_at(now_us))
195    }
196
197    /// The monotonic instant at which UTC reaches `utc_us`.
198    #[must_use]
199    pub fn mono_for_utc(&self, utc_us: i64) -> Option<u64> {
200        self.anchor.map(|a| a.mono_for_utc(utc_us))
201    }
202
203    /// Offer a measurement; the anchor changes per the replacement rule.
204    ///
205    /// A more trusted source replaces at once; a less trusted one only when
206    /// `policy.allow_downgrade`. For the same source: a reading that disagrees
207    /// with the prediction by more than the two error bars together is a clock
208    /// step and is adopted immediately; otherwise it replaces the anchor when
209    /// it is at least as precise or the anchor is older than
210    /// `policy.resync_us`.
211    pub fn offer(
212        &mut self,
213        sample: UtcSample,
214        source: ClockSource,
215        now_us: u64,
216        policy: OfferPolicy,
217    ) -> OfferOutcome {
218        let new = Anchor::from_sample(sample, source);
219        let Some(old) = self.anchor else {
220            self.anchor = Some(new);
221            return OfferOutcome::Adopted(AdoptReason::First);
222        };
223        let reason = if new.source > old.source {
224            AdoptReason::HigherTrust
225        } else if new.source < old.source {
226            if policy.allow_downgrade {
227                AdoptReason::Downgrade
228            } else {
229                return OfferOutcome::Rejected(RejectReason::LowerTrust);
230            }
231        } else {
232            let predicted = i128::from(old.utc_at(new.mono_us));
233            let diff = (i128::from(new.utc_us) - predicted).unsigned_abs();
234            let bars = u128::from(old.uncertainty_us) + u128::from(new.uncertainty_us);
235            if diff > bars {
236                AdoptReason::Step
237            } else if now_us.saturating_sub(old.mono_us) >= policy.resync_us {
238                AdoptReason::Stale
239            } else if new.uncertainty_us <= old.uncertainty_us {
240                AdoptReason::Refined
241            } else {
242                return OfferOutcome::Rejected(RejectReason::LessPrecise);
243            }
244        };
245        self.anchor = Some(new);
246        OfferOutcome::Adopted(reason)
247    }
248}