Skip to main content

reliakit_core/
lib.rs

1//! Shared building blocks for the Reliakit workspace.
2//!
3//! Reliakit's resilience crates ([`reliakit-backoff`], [`reliakit-circuit`],
4//! [`reliakit-ratelimit`], [`reliakit-timeout`]) are *clock-agnostic*: you pass
5//! the current time in as a `u64` tick in any monotonic unit you choose. This
6//! crate provides the small piece they have in common — a [`Clock`] trait and a
7//! couple of ready-made clocks — so you do not have to hand-roll one.
8//!
9//! - [`ManualClock`] — a settable clock for deterministic tests; `no_std`.
10//! - [`MonotonicClock`] — wall-free monotonic milliseconds (requires `std`).
11//!
12//! The crate has no dependencies and forbids unsafe code. The `Clock` trait and
13//! [`ManualClock`] are available on `no_std`; [`MonotonicClock`] needs the
14//! default `std` feature.
15//!
16//! # Example
17//!
18//! ```
19//! use reliakit_core::{Clock, ManualClock};
20//!
21//! let clock = ManualClock::new(0);
22//! assert_eq!(clock.now(), 0);
23//! clock.advance(250);
24//! assert_eq!(clock.now(), 250);
25//!
26//! // Feed `clock.now()` into any clock-agnostic Reliakit policy.
27//! fn elapsed_since<C: Clock>(clock: &C, start: u64) -> u64 {
28//!     clock.now().saturating_sub(start)
29//! }
30//! assert_eq!(elapsed_since(&clock, 100), 150);
31//! ```
32//!
33//! [`reliakit-backoff`]: https://docs.rs/reliakit-backoff
34//! [`reliakit-circuit`]: https://docs.rs/reliakit-circuit
35//! [`reliakit-ratelimit`]: https://docs.rs/reliakit-ratelimit
36//! [`reliakit-timeout`]: https://docs.rs/reliakit-timeout
37
38#![cfg_attr(not(feature = "std"), no_std)]
39#![forbid(unsafe_code)]
40#![warn(missing_docs)]
41
42use core::cell::Cell;
43
44/// A source of monotonic time, expressed as a `u64` tick.
45///
46/// The unit is whatever the clock uses (milliseconds is typical) and must match
47/// the unit you pass to the resilience policies. Implementations should be
48/// monotonic non-decreasing, though Reliakit's policies all saturate and so do
49/// not panic if a clock moves backwards.
50pub trait Clock {
51    /// Returns the current time as a `u64` tick.
52    fn now(&self) -> u64;
53}
54
55impl<C: Clock + ?Sized> Clock for &C {
56    fn now(&self) -> u64 {
57        (**self).now()
58    }
59}
60
61/// A clock whose time is set explicitly, for deterministic tests.
62///
63/// Uses interior mutability so [`now`](Clock::now), [`set`](Self::set), and
64/// [`advance`](Self::advance) all take `&self`.
65#[derive(Debug, Default)]
66pub struct ManualClock {
67    tick: Cell<u64>,
68}
69
70impl ManualClock {
71    /// Creates a clock reading `start`.
72    pub const fn new(start: u64) -> Self {
73        Self {
74            tick: Cell::new(start),
75        }
76    }
77
78    /// Sets the clock to `tick`.
79    pub fn set(&self, tick: u64) {
80        self.tick.set(tick);
81    }
82
83    /// Advances the clock by `delta` (saturating).
84    pub fn advance(&self, delta: u64) {
85        self.tick.set(self.tick.get().saturating_add(delta));
86    }
87}
88
89impl Clock for ManualClock {
90    fn now(&self) -> u64 {
91        self.tick.get()
92    }
93}
94
95/// A monotonic clock measuring milliseconds since it was created.
96///
97/// Backed by [`std::time::Instant`], so it never goes backwards and is immune to
98/// wall-clock adjustments. Requires the `std` feature.
99#[cfg(feature = "std")]
100#[derive(Debug, Clone)]
101pub struct MonotonicClock {
102    origin: std::time::Instant,
103}
104
105#[cfg(feature = "std")]
106impl MonotonicClock {
107    /// Creates a clock whose zero point is now.
108    pub fn new() -> Self {
109        Self {
110            origin: std::time::Instant::now(),
111        }
112    }
113}
114
115#[cfg(feature = "std")]
116impl Default for MonotonicClock {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122#[cfg(feature = "std")]
123impl Clock for MonotonicClock {
124    fn now(&self) -> u64 {
125        // Milliseconds since creation, saturating rather than wrapping on the
126        // (astronomically unlikely) overflow of a u64 millisecond count.
127        let millis = self.origin.elapsed().as_millis();
128        if millis > u64::MAX as u128 {
129            u64::MAX
130        } else {
131            millis as u64
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn manual_clock_reads_sets_and_advances() {
142        let clock = ManualClock::new(10);
143        assert_eq!(clock.now(), 10);
144        clock.advance(5);
145        assert_eq!(clock.now(), 15);
146        clock.set(100);
147        assert_eq!(clock.now(), 100);
148    }
149
150    #[test]
151    fn manual_clock_advance_saturates() {
152        let clock = ManualClock::new(u64::MAX - 1);
153        clock.advance(10);
154        assert_eq!(clock.now(), u64::MAX);
155    }
156
157    #[test]
158    fn manual_clock_default_is_zero() {
159        assert_eq!(ManualClock::default().now(), 0);
160    }
161
162    #[test]
163    fn clock_is_object_safe_and_reference_forwards() {
164        let clock = ManualClock::new(7);
165        let by_ref: &dyn Clock = &clock;
166        assert_eq!(by_ref.now(), 7);
167        // The blanket impl on `&C` lets a `&ManualClock` be used as a `Clock`.
168        fn read(c: impl Clock) -> u64 {
169            c.now()
170        }
171        assert_eq!(read(&clock), 7);
172    }
173
174    #[cfg(feature = "std")]
175    #[test]
176    fn monotonic_clock_is_non_decreasing() {
177        let clock = MonotonicClock::new();
178        let a = clock.now();
179        let b = clock.now();
180        assert!(b >= a);
181        assert!(MonotonicClock::default().now() < u64::MAX);
182    }
183}