Skip to main content

clock_lib/
clock.rs

1//! Mockable clock abstraction.
2//!
3//! The [`Clock`] trait lets time-driven code — rate limiters, TTL caches,
4//! timeouts, retry backoff — depend on an injected source of time instead
5//! of calling the OS directly. Production code uses [`SystemClock`], which
6//! simply forwards to the OS. Tests use [`ManualClock`], which advances on
7//! demand so timing-driven behavior can be exercised deterministically
8//! without ever calling [`std::thread::sleep`].
9
10#[cfg(feature = "std")]
11use core::sync::atomic::{AtomicU64, Ordering};
12#[cfg(feature = "std")]
13use core::time::Duration;
14#[cfg(feature = "std")]
15use std::sync::Arc;
16#[cfg(feature = "std")]
17use std::time::{Instant, SystemTime};
18
19#[cfg(feature = "std")]
20use crate::{Monotonic, Wall};
21
22/// A source of time.
23///
24/// `Clock` exposes both kinds of reading [`clock-lib`](crate) draws apart:
25/// a monotonic reading via [`Clock::now`] and a wall-clock reading via
26/// [`Clock::wall`]. Implementations must be safe to share across threads.
27///
28/// In production, take a `Clock` (or `&dyn Clock`, or `Arc<dyn Clock>`) as
29/// a dependency and use [`SystemClock`] at the top of your call graph. In
30/// tests, substitute [`ManualClock`] and drive time forward by calling
31/// [`ManualClock::advance`].
32///
33/// `Clock` is also implemented for `Arc<C>` and `&C` where `C: Clock`,
34/// so the same value can be shared and reused freely.
35///
36/// # Examples
37///
38/// ```
39/// use clock_lib::{Clock, ManualClock, SystemClock};
40/// use std::time::Duration;
41///
42/// fn took_at_least<C: Clock>(clock: &C, start: clock_lib::Monotonic, target: Duration) -> bool {
43///     clock.now().duration_since(start) >= target
44/// }
45///
46/// // Production
47/// let sys = SystemClock::new();
48/// let start = sys.now();
49/// assert!(!took_at_least(&sys, start, Duration::from_secs(60 * 60)));
50///
51/// // Test — no sleep, fully deterministic
52/// let test = ManualClock::new();
53/// let start = test.now();
54/// test.advance(Duration::from_secs(60 * 60));
55/// assert!(took_at_least(&test, start, Duration::from_secs(60 * 60)));
56/// ```
57#[cfg(feature = "std")]
58#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
59pub trait Clock: Send + Sync {
60    /// Returns the current monotonic reading.
61    fn now(&self) -> Monotonic;
62
63    /// Returns the current wall-clock reading.
64    fn wall(&self) -> Wall;
65}
66
67/// A clock backed by the operating system.
68///
69/// `SystemClock` forwards [`Clock::now`] to [`Monotonic::now`] and
70/// [`Clock::wall`] to [`Wall::now`]. It is zero-sized, `Copy`, and the
71/// constructor is `const`, so it imposes no runtime cost over calling the
72/// free functions directly.
73///
74/// # Examples
75///
76/// ```
77/// use clock_lib::{Clock, SystemClock};
78///
79/// const CLOCK: SystemClock = SystemClock::new();
80/// let t = CLOCK.now();
81/// # let _ = t;
82/// ```
83#[cfg(feature = "std")]
84#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
85#[derive(Debug, Default, Copy, Clone)]
86pub struct SystemClock;
87
88#[cfg(feature = "std")]
89impl SystemClock {
90    /// Constructs a new system clock.
91    #[inline]
92    #[must_use]
93    pub const fn new() -> Self {
94        Self
95    }
96}
97
98#[cfg(feature = "std")]
99impl Clock for SystemClock {
100    #[inline]
101    fn now(&self) -> Monotonic {
102        Monotonic::now()
103    }
104
105    #[inline]
106    fn wall(&self) -> Wall {
107        Wall::now()
108    }
109}
110
111/// A clock under your control, for deterministic testing.
112///
113/// `ManualClock` captures the OS monotonic and wall-clock anchors at
114/// construction, then advances **only** when you call
115/// [`advance`](Self::advance). The clock never moves on its own, so tests
116/// against time-driven code become point-in-time deterministic.
117///
118/// `ManualClock` is `Send + Sync`. Wrap it in an [`Arc`] to share with the
119/// code under test; both the test driver and the production code can hold
120/// references to the same clock and observe consistent readings.
121///
122/// # Examples
123///
124/// ```
125/// use clock_lib::{Clock, ManualClock, Monotonic};
126/// use std::sync::Arc;
127/// use std::time::Duration;
128///
129/// // A "ttl expired?" check that can be driven without sleeping.
130/// fn expired<C: Clock>(clock: &C, stamp: Monotonic, ttl: Duration) -> bool {
131///     clock.now().duration_since(stamp) >= ttl
132/// }
133///
134/// let clock = Arc::new(ManualClock::new());
135/// let stamp = clock.now();
136///
137/// assert!(!expired(&*clock, stamp, Duration::from_secs(60)));
138///
139/// clock.advance(Duration::from_secs(60));
140/// assert!(expired(&*clock, stamp, Duration::from_secs(60)));
141/// ```
142#[cfg(feature = "std")]
143#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
144#[derive(Debug)]
145pub struct ManualClock {
146    monotonic_anchor: Instant,
147    wall_anchor: SystemTime,
148    offset_nanos: AtomicU64,
149}
150
151#[cfg(feature = "std")]
152impl ManualClock {
153    /// Constructs a new manual clock anchored at the current OS time.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use clock_lib::ManualClock;
159    ///
160    /// let clock = ManualClock::new();
161    /// # let _ = clock;
162    /// ```
163    #[inline]
164    #[must_use]
165    pub fn new() -> Self {
166        Self {
167            monotonic_anchor: Instant::now(),
168            wall_anchor: SystemTime::now(),
169            offset_nanos: AtomicU64::new(0),
170        }
171    }
172
173    /// Advances the clock forward by `by`.
174    ///
175    /// Successive calls accumulate. If the cumulative offset would exceed
176    /// [`u64::MAX`] nanoseconds (≈584 years), the offset saturates &mdash;
177    /// well outside any plausible test scenario.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use clock_lib::{Clock, ManualClock};
183    /// use std::time::Duration;
184    ///
185    /// let clock = ManualClock::new();
186    /// let a = clock.now();
187    /// clock.advance(Duration::from_secs(5));
188    /// let b = clock.now();
189    /// assert_eq!(b.duration_since(a), Duration::from_secs(5));
190    /// ```
191    #[inline]
192    pub fn advance(&self, by: Duration) {
193        let nanos = u64::try_from(by.as_nanos()).unwrap_or(u64::MAX);
194        let _ = self.offset_nanos.fetch_add(nanos, Ordering::Relaxed);
195    }
196
197    /// Returns the cumulative offset that has been added to this clock
198    /// since it was constructed.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use clock_lib::ManualClock;
204    /// use std::time::Duration;
205    ///
206    /// let clock = ManualClock::new();
207    /// clock.advance(Duration::from_secs(1));
208    /// clock.advance(Duration::from_secs(2));
209    /// assert_eq!(clock.offset(), Duration::from_secs(3));
210    /// ```
211    #[inline]
212    #[must_use]
213    pub fn offset(&self) -> Duration {
214        Duration::from_nanos(self.offset_nanos.load(Ordering::Relaxed))
215    }
216}
217
218#[cfg(feature = "std")]
219impl Default for ManualClock {
220    #[inline]
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226#[cfg(feature = "std")]
227impl Clock for ManualClock {
228    /// Returns the monotonic anchor plus the accumulated offset.
229    ///
230    /// # Panics
231    ///
232    /// Panics if the anchor plus offset is not representable as an
233    /// [`Instant`] on the current platform. This requires a cumulative
234    /// offset measured in centuries and never occurs in realistic tests.
235    fn now(&self) -> Monotonic {
236        let offset = Duration::from_nanos(self.offset_nanos.load(Ordering::Relaxed));
237        Monotonic(self.monotonic_anchor + offset)
238    }
239
240    /// Returns the wall-clock anchor plus the accumulated offset.
241    ///
242    /// # Panics
243    ///
244    /// Panics if the anchor plus offset is not representable as a
245    /// [`SystemTime`] on the current platform. This requires a cumulative
246    /// offset measured in centuries and never occurs in realistic tests.
247    fn wall(&self) -> Wall {
248        let offset = Duration::from_nanos(self.offset_nanos.load(Ordering::Relaxed));
249        Wall(self.wall_anchor + offset)
250    }
251}
252
253#[cfg(feature = "std")]
254impl<C: Clock + ?Sized> Clock for Arc<C> {
255    #[inline]
256    fn now(&self) -> Monotonic {
257        (**self).now()
258    }
259
260    #[inline]
261    fn wall(&self) -> Wall {
262        (**self).wall()
263    }
264}
265
266#[cfg(feature = "std")]
267impl<C: Clock + ?Sized> Clock for &C {
268    #[inline]
269    fn now(&self) -> Monotonic {
270        (**self).now()
271    }
272
273    #[inline]
274    fn wall(&self) -> Wall {
275        (**self).wall()
276    }
277}