lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

time.rs (8341B)


      1 //! Injected wall and monotonic clock contracts.
      2 
      3 use core::fmt;
      4 use std::{error::Error, time::Duration};
      5 
      6 /// A nonnegative whole-second UTC timestamp relative to the Unix epoch.
      7 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
      8 pub struct UnixTimeSeconds(u64);
      9 
     10 impl UnixTimeSeconds {
     11     /// Creates an already validated Unix timestamp.
     12     #[must_use]
     13     pub const fn new(seconds: u64) -> Self {
     14         Self(seconds)
     15     }
     16 
     17     /// Returns whole seconds since the Unix epoch.
     18     #[must_use]
     19     pub const fn get(self) -> u64 {
     20         self.0
     21     }
     22 }
     23 
     24 /// A process-local monotonic observation relative to one clock origin.
     25 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
     26 pub struct MonotonicTime(Duration);
     27 
     28 impl MonotonicTime {
     29     /// Creates a monotonic observation from a clock-relative duration.
     30     #[must_use]
     31     pub const fn from_duration_since_origin(elapsed: Duration) -> Self {
     32         Self(elapsed)
     33     }
     34 
     35     /// Returns the clock-relative duration.
     36     #[must_use]
     37     pub const fn duration_since_origin(self) -> Duration {
     38         self.0
     39     }
     40 
     41     /// Computes a deadline without wrapping on duration overflow.
     42     pub fn checked_deadline_after(
     43         self,
     44         duration: Duration,
     45     ) -> Result<MonotonicDeadline, MonotonicClockError> {
     46         self.0
     47             .checked_add(duration)
     48             .map(|elapsed| MonotonicDeadline(Self(elapsed)))
     49             .ok_or(MonotonicClockError::DeadlineOverflow)
     50     }
     51 }
     52 
     53 /// A deadline in the same process-local clock domain as `MonotonicTime`.
     54 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
     55 pub struct MonotonicDeadline(MonotonicTime);
     56 
     57 impl MonotonicDeadline {
     58     /// Returns true when the supplied observation reaches or passes the deadline.
     59     #[must_use]
     60     pub fn is_reached_at(self, now: MonotonicTime) -> bool {
     61         now.0 >= self.0.0
     62     }
     63 
     64     /// Returns the deadline as a clock-relative observation.
     65     #[must_use]
     66     pub const fn time(self) -> MonotonicTime {
     67         self.0
     68     }
     69 }
     70 
     71 /// Wall-clock adapter failure.
     72 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     73 pub enum WallClockError {
     74     BeforeUnixEpoch,
     75 }
     76 
     77 impl fmt::Display for WallClockError {
     78     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     79         match self {
     80             Self::BeforeUnixEpoch => formatter.write_str("wall clock is before the Unix epoch"),
     81         }
     82     }
     83 }
     84 
     85 impl Error for WallClockError {}
     86 
     87 /// Monotonic clock arithmetic failure.
     88 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     89 pub enum MonotonicClockError {
     90     DeadlineOverflow,
     91 }
     92 
     93 impl fmt::Display for MonotonicClockError {
     94     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     95         match self {
     96             Self::DeadlineOverflow => formatter.write_str("monotonic deadline overflows"),
     97         }
     98     }
     99 }
    100 
    101 impl Error for MonotonicClockError {}
    102 
    103 /// Injected source of restart-stable wall UTC observations.
    104 pub trait WallClock: Send + Sync {
    105     /// Returns the current whole-second UTC timestamp.
    106     fn now_utc(&self) -> Result<UnixTimeSeconds, WallClockError>;
    107 }
    108 
    109 /// Injected source of process-local monotonic observations.
    110 pub trait MonotonicClock: Send + Sync {
    111     /// Returns the current observation in this clock's domain.
    112     fn now_monotonic(&self) -> MonotonicTime;
    113 
    114     /// Computes a deadline relative to the current observation.
    115     fn deadline_after(&self, duration: Duration) -> Result<MonotonicDeadline, MonotonicClockError> {
    116         self.now_monotonic().checked_deadline_after(duration)
    117     }
    118 }
    119 
    120 /// Production wall clock backed by `SystemTime`.
    121 #[derive(Clone, Copy, Debug, Default)]
    122 pub struct SystemWallClock;
    123 
    124 impl WallClock for SystemWallClock {
    125     fn now_utc(&self) -> Result<UnixTimeSeconds, WallClockError> {
    126         std::time::SystemTime::now()
    127             .duration_since(std::time::UNIX_EPOCH)
    128             .map(|duration| UnixTimeSeconds::new(duration.as_secs()))
    129             .map_err(|_| WallClockError::BeforeUnixEpoch)
    130     }
    131 }
    132 
    133 /// Production monotonic clock with an instance-local origin.
    134 #[derive(Clone, Copy, Debug)]
    135 pub struct SystemMonotonicClock {
    136     origin: std::time::Instant,
    137 }
    138 
    139 impl SystemMonotonicClock {
    140     /// Captures a new private monotonic origin.
    141     #[must_use]
    142     pub fn new() -> Self {
    143         Self {
    144             origin: std::time::Instant::now(),
    145         }
    146     }
    147 }
    148 
    149 impl Default for SystemMonotonicClock {
    150     fn default() -> Self {
    151         Self::new()
    152     }
    153 }
    154 
    155 impl MonotonicClock for SystemMonotonicClock {
    156     fn now_monotonic(&self) -> MonotonicTime {
    157         MonotonicTime::from_duration_since_origin(self.origin.elapsed())
    158     }
    159 }
    160 
    161 #[cfg(test)]
    162 mod tests {
    163     use core::sync::atomic::{AtomicU64, Ordering};
    164 
    165     use super::*;
    166 
    167     struct FakeClock {
    168         wall_seconds: AtomicU64,
    169         monotonic_millis: AtomicU64,
    170     }
    171 
    172     impl FakeClock {
    173         fn new(wall_seconds: u64, monotonic_millis: u64) -> Self {
    174             Self {
    175                 wall_seconds: AtomicU64::new(wall_seconds),
    176                 monotonic_millis: AtomicU64::new(monotonic_millis),
    177             }
    178         }
    179 
    180         fn advance(&self, duration: Duration) {
    181             self.wall_seconds
    182                 .fetch_add(duration.as_secs(), Ordering::Relaxed);
    183             let millis = u64::try_from(duration.as_millis()).expect("test duration fits u64");
    184             self.monotonic_millis.fetch_add(millis, Ordering::Relaxed);
    185         }
    186     }
    187 
    188     impl WallClock for FakeClock {
    189         fn now_utc(&self) -> Result<UnixTimeSeconds, WallClockError> {
    190             Ok(UnixTimeSeconds::new(
    191                 self.wall_seconds.load(Ordering::Relaxed),
    192             ))
    193         }
    194     }
    195 
    196     impl MonotonicClock for FakeClock {
    197         fn now_monotonic(&self) -> MonotonicTime {
    198             MonotonicTime::from_duration_since_origin(Duration::from_millis(
    199                 self.monotonic_millis.load(Ordering::Relaxed),
    200             ))
    201         }
    202     }
    203 
    204     #[test]
    205     fn fake_clocks_advance_without_hidden_system_reads() {
    206         let clock = FakeClock::new(1_000, 40);
    207         let deadline = clock
    208             .deadline_after(Duration::from_millis(25))
    209             .expect("deadline");
    210 
    211         assert_eq!(clock.now_utc().expect("wall time").get(), 1_000);
    212         assert!(!deadline.is_reached_at(clock.now_monotonic()));
    213         clock.advance(Duration::from_millis(25));
    214         assert!(deadline.is_reached_at(clock.now_monotonic()));
    215         assert_eq!(clock.now_utc().expect("wall time").get(), 1_000);
    216         clock.advance(Duration::from_secs(2));
    217         assert_eq!(clock.now_utc().expect("wall time").get(), 1_002);
    218     }
    219 
    220     #[test]
    221     fn deadline_comparison_is_inclusive_and_overflow_is_rejected() {
    222         let now = MonotonicTime::from_duration_since_origin(Duration::from_secs(5));
    223         let deadline = now
    224             .checked_deadline_after(Duration::from_secs(2))
    225             .expect("deadline");
    226         assert!(!deadline.is_reached_at(now));
    227         assert!(
    228             deadline.is_reached_at(MonotonicTime::from_duration_since_origin(
    229                 Duration::from_secs(7)
    230             ))
    231         );
    232         assert!(
    233             deadline.is_reached_at(MonotonicTime::from_duration_since_origin(
    234                 Duration::from_secs(8)
    235             ))
    236         );
    237 
    238         assert_eq!(
    239             MonotonicTime::from_duration_since_origin(Duration::MAX)
    240                 .checked_deadline_after(Duration::from_nanos(1)),
    241             Err(MonotonicClockError::DeadlineOverflow)
    242         );
    243     }
    244 
    245     #[test]
    246     fn production_clock_adapters_smoke_test() {
    247         assert!(SystemWallClock.now_utc().expect("system wall time").get() > 0);
    248 
    249         let monotonic = SystemMonotonicClock::new();
    250         let first = monotonic.now_monotonic();
    251         let second = monotonic.now_monotonic();
    252         assert!(second >= first);
    253         assert!(monotonic.deadline_after(Duration::from_secs(1)).is_ok());
    254 
    255         let default_monotonic = SystemMonotonicClock::default();
    256         assert!(
    257             default_monotonic.now_monotonic().duration_since_origin() <= Duration::from_secs(1)
    258         );
    259         assert_eq!(
    260             WallClockError::BeforeUnixEpoch.to_string(),
    261             "wall clock is before the Unix epoch"
    262         );
    263         assert_eq!(
    264             MonotonicClockError::DeadlineOverflow.to_string(),
    265             "monotonic deadline overflows"
    266         );
    267     }
    268 }