Skip to main content

minip2p_platform/
deadline.rs

1use core::fmt;
2
3use crate::Now;
4
5/// A point on a clock's monotonic timeline, in milliseconds.
6///
7/// Caller-driven components report deadlines so the host knows how long it may
8/// idle before polling again. A deadline is only comparable to [`Now`] samples
9/// from the same [`Clock`](crate::Clock), since monotonic epochs are arbitrary.
10///
11/// Ordering is chronological, so the earliest deadline in a collection is its
12/// minimum and [`NEVER`](Self::NEVER) sorts last.
13///
14/// # Absent versus distant deadlines
15///
16/// "Nothing scheduled" is expressed as `Option::None`, not as `NEVER`. `NEVER`
17/// is the saturating result of arithmetic that overflows past the end of the
18/// timeline, and it never expires.
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct Deadline(u64);
21
22impl Deadline {
23    /// A deadline that never expires.
24    ///
25    /// Produced by saturating arithmetic such as
26    /// [`Now::deadline_after`](Now::deadline_after) with a huge delay.
27    pub const NEVER: Self = Self(u64::MAX);
28
29    /// A deadline that has already expired.
30    ///
31    /// Expired at every point on every timeline, so a component with work
32    /// buffered can report "poll me again without idling" without knowing what
33    /// the host's clock currently reads.
34    pub const IMMEDIATE: Self = Self(0);
35
36    /// Creates a deadline that expires when monotonic time reaches `millis`.
37    pub const fn from_millis(millis: u64) -> Self {
38        Self(millis)
39    }
40
41    /// Returns the monotonic milliseconds value this deadline expires at.
42    pub const fn as_millis(self) -> u64 {
43        self.0
44    }
45
46    /// Returns whether this deadline never expires.
47    pub const fn is_never(self) -> bool {
48        self.0 == u64::MAX
49    }
50
51    /// Returns whether this deadline has expired as of `now`.
52    ///
53    /// [`NEVER`](Self::NEVER) is never expired, even at the end of the
54    /// timeline.
55    pub const fn is_expired_at(self, now: Now) -> bool {
56        !self.is_never() && now.monotonic_ms >= self.0
57    }
58
59    /// Returns the milliseconds remaining until this deadline, or zero if it
60    /// has already expired.
61    ///
62    /// [`NEVER`](Self::NEVER) always reports `u64::MAX` remaining, however far
63    /// monotonic time has advanced, so it stays consistent with
64    /// [`is_expired_at`](Self::is_expired_at) never reporting it as due.
65    pub const fn millis_until(self, now: Now) -> u64 {
66        if self.is_never() {
67            return u64::MAX;
68        }
69        self.0.saturating_sub(now.monotonic_ms)
70    }
71
72    /// Returns a deadline `millis` later, saturating at
73    /// [`NEVER`](Self::NEVER).
74    pub const fn saturating_add_millis(self, millis: u64) -> Self {
75        Self(self.0.saturating_add(millis))
76    }
77
78    /// Returns whichever of the two deadlines comes first.
79    pub const fn earliest(self, other: Self) -> Self {
80        if self.0 <= other.0 { self } else { other }
81    }
82
83    /// Merges two optional deadlines, keeping whichever comes first.
84    ///
85    /// Useful for folding the deadlines of several subsystems into the one a
86    /// runtime reports to its host.
87    pub const fn earliest_opt(left: Option<Self>, right: Option<Self>) -> Option<Self> {
88        match (left, right) {
89            (Some(left), Some(right)) => Some(left.earliest(right)),
90            (Some(only), None) | (None, Some(only)) => Some(only),
91            (None, None) => None,
92        }
93    }
94}
95
96impl fmt::Display for Deadline {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        if self.is_never() {
99            f.write_str("never")
100        } else {
101            write!(f, "{}ms", self.0)
102        }
103    }
104}
105
106impl From<u64> for Deadline {
107    fn from(value: u64) -> Self {
108        Self::from_millis(value)
109    }
110}
111
112impl From<Deadline> for u64 {
113    fn from(value: Deadline) -> Self {
114        value.0
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use alloc::format;
122
123    #[test]
124    fn expires_once_now_reaches_it() {
125        let deadline = Deadline::from_millis(100);
126        assert!(!deadline.is_expired_at(Now::from_millis(99)));
127        assert!(deadline.is_expired_at(Now::from_millis(100)));
128        assert!(deadline.is_expired_at(Now::from_millis(101)));
129    }
130
131    #[test]
132    fn never_does_not_expire_at_end_of_timeline() {
133        assert!(Deadline::NEVER.is_never());
134        assert!(!Deadline::NEVER.is_expired_at(Now::from_millis(u64::MAX)));
135    }
136
137    #[test]
138    fn never_reports_full_remaining_time_however_far_now_has_advanced() {
139        // A plain saturating subtraction would shrink this toward zero as time
140        // passes while `is_expired_at` still reported "not due", so a host
141        // idling for `millis_until` would wake early for no reason.
142        for now in [0, 1, 1_000_000, u64::MAX / 2, u64::MAX - 1, u64::MAX] {
143            let now = Now::from_millis(now);
144            assert_eq!(
145                Deadline::NEVER.millis_until(now),
146                u64::MAX,
147                "millis_until disagreed with is_expired_at at {now:?}"
148            );
149            assert!(!Deadline::NEVER.is_expired_at(now));
150        }
151    }
152
153    #[test]
154    fn immediate_is_always_due() {
155        for now in [0, 1, 1_000_000, u64::MAX] {
156            let now = Now::from_millis(now);
157            assert!(
158                Deadline::IMMEDIATE.is_expired_at(now),
159                "IMMEDIATE must be due at {now:?}"
160            );
161            assert_eq!(Deadline::IMMEDIATE.millis_until(now), 0);
162        }
163        // Sorts ahead of any real deadline when folding subsystem deadlines.
164        assert_eq!(
165            Deadline::IMMEDIATE.earliest(Deadline::from_millis(5)),
166            Deadline::IMMEDIATE
167        );
168    }
169
170    #[test]
171    fn remaining_time_saturates_at_zero() {
172        // Raw millis are read back as an instant, not a duration.
173        let deadline = Deadline::from(100u64);
174        assert_eq!(deadline.as_millis(), 100);
175        assert_eq!(u64::from(deadline), 100);
176
177        assert_eq!(deadline.millis_until(Now::from_millis(40)), 60);
178        assert_eq!(deadline.millis_until(Now::from_millis(100)), 0);
179        assert_eq!(deadline.millis_until(Now::from_millis(500)), 0);
180    }
181
182    #[test]
183    fn adding_saturates_at_never() {
184        assert_eq!(
185            Deadline::from_millis(10).saturating_add_millis(5),
186            Deadline::from_millis(15)
187        );
188        assert_eq!(
189            Deadline::from_millis(10).saturating_add_millis(u64::MAX),
190            Deadline::NEVER
191        );
192    }
193
194    #[test]
195    fn earliest_picks_the_sooner_deadline() {
196        let soon = Deadline::from_millis(10);
197        let late = Deadline::from_millis(20);
198        assert_eq!(soon.earliest(late), soon);
199        assert_eq!(late.earliest(soon), soon);
200        assert_eq!(soon.earliest(Deadline::NEVER), soon);
201    }
202
203    #[test]
204    fn earliest_opt_treats_none_as_unscheduled() {
205        let soon = Some(Deadline::from_millis(10));
206        let late = Some(Deadline::from_millis(20));
207        assert_eq!(Deadline::earliest_opt(soon, late), soon);
208        assert_eq!(Deadline::earliest_opt(late, soon), soon);
209        assert_eq!(Deadline::earliest_opt(soon, None), soon);
210        assert_eq!(Deadline::earliest_opt(None, late), late);
211        assert_eq!(Deadline::earliest_opt(None, None), None);
212    }
213
214    #[test]
215    fn ordering_is_chronological_with_never_last() {
216        let mut deadlines = [
217            Deadline::NEVER,
218            Deadline::from_millis(30),
219            Deadline::from_millis(10),
220        ];
221        deadlines.sort();
222        assert_eq!(
223            deadlines,
224            [
225                Deadline::from_millis(10),
226                Deadline::from_millis(30),
227                Deadline::NEVER
228            ]
229        );
230        assert_eq!(
231            deadlines.iter().copied().min(),
232            Some(Deadline::from_millis(10))
233        );
234    }
235
236    #[test]
237    fn display_names_the_never_sentinel() {
238        assert_eq!(format!("{}", Deadline::from_millis(25)), "25ms");
239        assert_eq!(format!("{}", Deadline::NEVER), "never");
240    }
241}