dev_chaos/clock.rs
1//! Deterministic time-skew injection for testing time-sensitive code.
2//!
3//! Code that retries on timeout, expires sessions, schedules futures,
4//! or compares timestamps depends on a clock. [`Clock`] is a source
5//! of `Instant`-ish values that the caller controls explicitly: you
6//! advance it with [`Clock::advance`] or skew it with
7//! [`Clock::skew_by`] to validate that retry loops, expiry checks,
8//! and TTL logic behave correctly without `std::thread::sleep`.
9//!
10//! ## Determinism
11//!
12//! `Clock` is fully deterministic: the same sequence of `advance`
13//! and `now` calls produces the same sequence of values across runs
14//! and machines. No system calls, no thread sleeps.
15//!
16//! ## Pairing with `std::time::Instant`
17//!
18//! `Instant` is opaque and cannot be constructed directly, so this
19//! module uses an offset-from-anchor model. `Clock::now` returns a
20//! [`ClockTime`] (just a `Duration` from anchor); the caller adapts
21//! this to their domain. For callers that need an `Instant`, see
22//! [`Clock::anchor`] and add the offset.
23
24use std::sync::atomic::{AtomicI64, Ordering};
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27
28/// A virtual time value measured as a `Duration` since the clock's anchor.
29///
30/// Internally just a wrapper around `Duration`; cheap to copy.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct ClockTime(pub Duration);
33
34impl ClockTime {
35 /// The duration since the clock's anchor.
36 pub fn since_anchor(&self) -> Duration {
37 self.0
38 }
39}
40
41/// Deterministic, in-process clock for chaos testing.
42///
43/// `Clock` does **not** advance automatically. Callers explicitly
44/// move the clock forward via [`advance`](Clock::advance) and skew
45/// it via [`skew_by`](Clock::skew_by). This makes time-sensitive
46/// tests fully reproducible.
47///
48/// `Clock` is `Clone`-able and shares state with its clones via an
49/// internal `Arc<AtomicI64>` of nanoseconds-since-anchor; advancing
50/// or skewing one handle advances all. Safe to share across threads.
51///
52/// # Example
53///
54/// ```
55/// use dev_chaos::clock::Clock;
56/// use std::time::Duration;
57///
58/// let c = Clock::new();
59/// let t0 = c.now();
60/// c.advance(Duration::from_secs(5));
61/// let t1 = c.now();
62/// assert_eq!(t1.since_anchor() - t0.since_anchor(), Duration::from_secs(5));
63///
64/// // Skew negative to simulate clock going backward (e.g. NTP step).
65/// c.skew_by(-(Duration::from_secs(2).as_nanos() as i64));
66/// let t2 = c.now();
67/// assert!(t2 < t1);
68/// ```
69#[derive(Debug, Clone)]
70pub struct Clock {
71 /// Real wall-clock anchor when this clock was created. Used by
72 /// `anchor()` for callers that need to map back to `Instant`.
73 anchor: Instant,
74 /// Offset from the anchor in nanoseconds. Wrapped in `Arc` so
75 /// clones share state.
76 offset_ns: Arc<AtomicI64>,
77}
78
79impl Clock {
80 /// Build a new clock anchored at the current `Instant`.
81 pub fn new() -> Self {
82 Self {
83 anchor: Instant::now(),
84 offset_ns: Arc::new(AtomicI64::new(0)),
85 }
86 }
87
88 /// The real `Instant` this clock was anchored at.
89 ///
90 /// Callers that need to interoperate with code expecting `Instant`
91 /// can compute `clock.anchor() + clock.now().since_anchor()`.
92 pub fn anchor(&self) -> Instant {
93 self.anchor
94 }
95
96 /// Current virtual time, as offset from the anchor.
97 pub fn now(&self) -> ClockTime {
98 let ns = self.offset_ns.load(Ordering::Relaxed);
99 if ns >= 0 {
100 ClockTime(Duration::from_nanos(ns as u64))
101 } else {
102 // Negative offset: clamp to zero. Tests that observe a
103 // negative offset should use `now_signed` for the raw value.
104 ClockTime(Duration::ZERO)
105 }
106 }
107
108 /// Current virtual offset, signed (in nanoseconds from anchor).
109 ///
110 /// Useful when validating skew-backward scenarios where
111 /// `since_anchor()` would clamp.
112 pub fn now_signed_ns(&self) -> i64 {
113 self.offset_ns.load(Ordering::Relaxed)
114 }
115
116 /// Advance the clock by a non-negative `delta`.
117 ///
118 /// Equivalent to `skew_by(delta.as_nanos() as i64)` but rejects
119 /// negative durations at the type level.
120 pub fn advance(&self, delta: Duration) {
121 self.offset_ns
122 .fetch_add(delta.as_nanos() as i64, Ordering::Relaxed);
123 }
124
125 /// Skew the clock by `delta_ns`, which may be negative.
126 ///
127 /// Negative skew simulates clock-step events (e.g. NTP correction,
128 /// VM pause/resume backward jump). Tests can validate that retry
129 /// loops with timeout-based escape don't get stuck on negative
130 /// elapsed values.
131 pub fn skew_by(&self, delta_ns: i64) {
132 self.offset_ns.fetch_add(delta_ns, Ordering::Relaxed);
133 }
134
135 /// Reset the clock to anchor (offset zero).
136 pub fn reset(&self) {
137 self.offset_ns.store(0, Ordering::Relaxed);
138 }
139}
140
141impl Default for Clock {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn new_clock_starts_at_zero_offset() {
153 let c = Clock::new();
154 assert_eq!(c.now().since_anchor(), Duration::ZERO);
155 assert_eq!(c.now_signed_ns(), 0);
156 }
157
158 #[test]
159 fn advance_moves_forward() {
160 let c = Clock::new();
161 c.advance(Duration::from_millis(100));
162 c.advance(Duration::from_millis(50));
163 assert_eq!(c.now().since_anchor(), Duration::from_millis(150));
164 }
165
166 #[test]
167 fn skew_negative_clamps_now_to_zero() {
168 let c = Clock::new();
169 c.advance(Duration::from_millis(100));
170 c.skew_by(-(Duration::from_millis(200).as_nanos() as i64));
171 // After negative skew past zero, `now` clamps to zero.
172 assert_eq!(c.now().since_anchor(), Duration::ZERO);
173 // But the raw signed offset reflects the underlying value.
174 assert!(c.now_signed_ns() < 0);
175 }
176
177 #[test]
178 fn skew_negative_within_positive_offset_works() {
179 let c = Clock::new();
180 c.advance(Duration::from_millis(500));
181 c.skew_by(-(Duration::from_millis(100).as_nanos() as i64));
182 assert_eq!(c.now().since_anchor(), Duration::from_millis(400));
183 }
184
185 #[test]
186 fn cloned_clocks_share_state() {
187 let c = Clock::new();
188 let d = c.clone();
189 c.advance(Duration::from_secs(1));
190 assert_eq!(d.now().since_anchor(), Duration::from_secs(1));
191 }
192
193 #[test]
194 fn reset_returns_to_zero() {
195 let c = Clock::new();
196 c.advance(Duration::from_secs(10));
197 c.reset();
198 assert_eq!(c.now().since_anchor(), Duration::ZERO);
199 }
200
201 #[test]
202 fn anchor_is_preserved_across_clones() {
203 let c = Clock::new();
204 let d = c.clone();
205 assert_eq!(c.anchor(), d.anchor());
206 }
207
208 #[test]
209 fn deterministic_sequence_across_runs() {
210 // Same operations should produce same observable values, no
211 // matter how often we run.
212 let c = Clock::new();
213 c.advance(Duration::from_millis(100));
214 c.advance(Duration::from_millis(50));
215 c.skew_by(-(Duration::from_millis(20).as_nanos() as i64));
216 let observed = c.now().since_anchor();
217 // Run again with a fresh clock.
218 let c2 = Clock::new();
219 c2.advance(Duration::from_millis(100));
220 c2.advance(Duration::from_millis(50));
221 c2.skew_by(-(Duration::from_millis(20).as_nanos() as i64));
222 let observed2 = c2.now().since_anchor();
223 assert_eq!(observed, observed2);
224 }
225}