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 }