Skip to main content

reliakit_core/
lib.rs

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