nautilus_core/time.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! The core `AtomicTime` for real-time and static clocks.
17//!
18//! This module provides an atomic time abstraction that supports both real-time and static
19//! clocks. It ensures thread-safe operations and monotonic time retrieval with nanosecond precision.
20//!
21//! # Modes
22//!
23//! - **Real-time mode:** The clock continuously syncs with system wall-clock time (via
24//! [`SystemTime::now()`]). To ensure strict monotonic increments across multiple threads,
25//! the internal updates use an atomic compare-and-exchange loop (`time_since_epoch`).
26//! While this guarantees that every new timestamp is at least one nanosecond greater than the
27//! last, it may introduce higher contention if many threads call it heavily.
28//!
29//! - **Static mode:** The clock is manually controlled via [`AtomicTime::set_time`] or [`AtomicTime::increment_time`],
30//! which can be useful for simulations or backtesting. You can switch modes at runtime using
31//! [`AtomicTime::make_realtime`] or [`AtomicTime::make_static`]. In **static mode**, we use
32//! acquire/release semantics so that updates from one thread can be observed by another;
33//! however, we do not enforce strict global ordering for manual updates. If you need strong,
34//! multi-threaded ordering in **static mode**, you must coordinate higher-level synchronization yourself.
35
36use std::{
37 sync::{
38 OnceLock,
39 atomic::{AtomicBool, AtomicU64, Ordering},
40 },
41 time::{Duration, SystemTime, UNIX_EPOCH},
42};
43
44use crate::{
45 UnixNanos,
46 datetime::{NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND},
47};
48
49/// Global atomic time in **real-time mode** for use across the system.
50///
51/// This clock operates in **real-time mode**, synchronizing with the system clock.
52/// It provides globally unique, strictly increasing timestamps across threads.
53pub static ATOMIC_CLOCK_REALTIME: OnceLock<AtomicTime> = OnceLock::new();
54
55/// Global atomic time in **static mode** for use across the system.
56///
57/// This clock operates in **static mode**, where the time value can be set or incremented
58/// manually. Useful for backtesting or simulated time control.
59pub static ATOMIC_CLOCK_STATIC: OnceLock<AtomicTime> = OnceLock::new();
60
61/// Returns a static reference to the global atomic clock in **real-time mode**.
62///
63/// This clock uses [`AtomicTime::time_since_epoch`] under the hood, ensuring strictly increasing
64/// timestamps across threads.
65pub fn get_atomic_clock_realtime() -> &'static AtomicTime {
66 ATOMIC_CLOCK_REALTIME.get_or_init(AtomicTime::default)
67}
68
69/// Returns a static reference to the global atomic clock in **static mode**.
70///
71/// This clock allows manual time control via [`AtomicTime::set_time`] or [`AtomicTime::increment_time`],
72/// and does not automatically sync with system time.
73pub fn get_atomic_clock_static() -> &'static AtomicTime {
74 ATOMIC_CLOCK_STATIC.get_or_init(|| AtomicTime::new(false, UnixNanos::default()))
75}
76
77/// Returns the duration since the UNIX epoch based on [`SystemTime::now()`].
78///
79/// # Panics
80///
81/// Panics if the system time is set before the UNIX epoch.
82#[inline(always)]
83#[must_use]
84pub fn duration_since_unix_epoch() -> Duration {
85 // The expect() is acceptable here because:
86 // - SystemTime failure indicates catastrophic system clock issues
87 // - This would affect the entire application's ability to function
88 // - Alternative error handling would complicate all time-dependent code paths
89 // - Such failures are extremely rare in practice and indicate hardware/OS problems
90 wall_clock_now()
91 .duration_since(UNIX_EPOCH)
92 .expect("Error calling `SystemTime`")
93}
94
95/// Returns the current wall-clock time as [`SystemTime`].
96///
97/// Under simulation (`simulation` + `cfg(madsim)`), returns virtual wall-clock
98/// time from the madsim deterministic scheduler when called from inside a
99/// madsim runtime. When called outside a runtime (e.g. plain `#[rstest]` test
100/// bodies), falls back to [`SystemTime::now()`], which under `cfg(madsim)` is
101/// libc-intercepted by madsim and resolves to the same real syscall it would
102/// in a normal build. Under normal builds, returns [`SystemTime::now()`].
103///
104/// This is the wall-clock seam. It preserves Unix-epoch semantics (unlike
105/// `tokio::time::Instant` which is monotonic and carries no epoch).
106#[inline(always)]
107#[must_use]
108fn wall_clock_now() -> SystemTime {
109 #[cfg(not(all(feature = "simulation", madsim)))]
110 {
111 SystemTime::now()
112 }
113 #[cfg(all(feature = "simulation", madsim))]
114 {
115 // `try_current` returns `None` when no madsim runtime is active.
116 // Falling back to `SystemTime::now()` matches what madsim's own libc
117 // shim does for `clock_gettime` outside a runtime; production paths
118 // running under simulation are always inside a runtime, so they
119 // continue to receive virtual time.
120 match madsim::time::TimeHandle::try_current() {
121 Some(handle) => handle.now_time(),
122 None => SystemTime::now(),
123 }
124 }
125}
126
127/// Returns the current UNIX time in nanoseconds, based on [`SystemTime::now()`].
128///
129/// # Panics
130///
131/// Panics if the duration in nanoseconds exceeds `u64::MAX`.
132#[inline(always)]
133#[must_use]
134pub fn nanos_since_unix_epoch() -> u64 {
135 u64::try_from(duration_since_unix_epoch().as_nanos())
136 .expect("System time overflow: value exceeds u64::MAX nanoseconds")
137}
138
139/// Represents an atomic timekeeping structure.
140///
141/// [`AtomicTime`] can act as a real-time clock or static clock based on its mode.
142/// It uses an [`AtomicU64`] to atomically update the value using only immutable
143/// references.
144///
145/// The `realtime` flag indicates which mode the clock is currently in.
146/// For concurrency, this struct uses atomic operations with appropriate memory orderings:
147/// - **Acquire/Release** for reading/writing in **static mode**.
148/// - **Compare-and-exchange (`AcqRel`)** in real-time mode to guarantee monotonic increments.
149///
150/// The mode flag and timestamp are private so every update flows through the methods
151/// that uphold the monotonicity and mode invariants.
152#[repr(C)]
153#[derive(Debug)]
154pub struct AtomicTime {
155 realtime: AtomicBool,
156 timestamp_ns: AtomicU64,
157}
158
159impl Default for AtomicTime {
160 /// Creates a new default [`AtomicTime`] instance in **real-time mode**, starting at the current system time.
161 fn default() -> Self {
162 Self::new(true, UnixNanos::default())
163 }
164}
165
166impl AtomicTime {
167 /// Creates a new [`AtomicTime`] instance.
168 ///
169 /// - If `realtime` is `true`, the provided `time` is ignored and the first read starts from
170 /// the current system time.
171 /// - If `realtime` is `false`, this clock starts in **static mode**, with the given `time`
172 /// as its current value.
173 #[must_use]
174 pub fn new(realtime: bool, time: UnixNanos) -> Self {
175 let timestamp_ns = if realtime { 0 } else { time.into() };
176
177 Self {
178 realtime: AtomicBool::new(realtime),
179 timestamp_ns: AtomicU64::new(timestamp_ns),
180 }
181 }
182
183 /// Returns the current time in nanoseconds, based on the clock's mode.
184 ///
185 /// - In **real-time mode**, calls [`AtomicTime::time_since_epoch`], ensuring strictly increasing
186 /// timestamps across threads, using `AcqRel` semantics for the underlying atomic.
187 /// - In **static mode**, reads the stored time using [`Ordering::Acquire`]. Updates by other
188 /// threads using [`AtomicTime::set_time`] or [`AtomicTime::increment_time`] (Release/AcqRel)
189 /// will be visible here.
190 ///
191 /// # Thread Safety
192 ///
193 /// The mode check is not atomic with the subsequent read/update. If another thread
194 /// switches modes between the check and the operation, one stale-mode result may be
195 /// returned. This is intentional: mode switching is a setup-time operation and should
196 /// not occur concurrently with time operations.
197 #[must_use]
198 pub fn get_time_ns(&self) -> UnixNanos {
199 if self.realtime.load(Ordering::Acquire) {
200 self.time_since_epoch()
201 } else {
202 UnixNanos::from(self.timestamp_ns.load(Ordering::Acquire))
203 }
204 }
205
206 /// Returns the current time as microseconds.
207 #[must_use]
208 pub fn get_time_us(&self) -> u64 {
209 self.get_time_ns().as_u64() / NANOSECONDS_IN_MICROSECOND
210 }
211
212 /// Returns the current time as milliseconds.
213 #[must_use]
214 pub fn get_time_ms(&self) -> u64 {
215 self.get_time_ns().as_u64() / NANOSECONDS_IN_MILLISECOND
216 }
217
218 /// Returns the current time as seconds.
219 #[must_use]
220 #[expect(
221 clippy::cast_precision_loss,
222 reason = "Precision loss acceptable for time conversion"
223 )]
224 pub fn get_time(&self) -> f64 {
225 self.get_time_ns().as_f64() / (NANOSECONDS_IN_SECOND as f64)
226 }
227
228 /// Manually sets a new time for the clock (only possible in **static mode**).
229 ///
230 /// This uses an atomic store with [`Ordering::Release`], so any thread reading with
231 /// [`Ordering::Acquire`] will see the updated time. This does *not* enforce a total ordering
232 /// among all threads, but is enough to ensure that once a thread sees this update, it also
233 /// sees all writes made before this call in the writing thread.
234 ///
235 /// Typically used in single-threaded scenarios or coordinated concurrency in **static mode**,
236 /// since there's no global ordering across threads.
237 ///
238 /// # Panics
239 ///
240 /// Panics if invoked when in real-time mode.
241 ///
242 /// # Thread Safety
243 ///
244 /// The mode check is not atomic with the subsequent store. If another thread calls
245 /// `make_realtime()` between the check and store, the invariant can be violated.
246 /// This is intentional: mode switching is a setup-time operation and should not
247 /// occur concurrently with time operations. Callers must ensure mode switches are
248 /// complete before resuming time operations.
249 pub fn set_time(&self, time: UnixNanos) {
250 assert!(
251 !self.realtime.load(Ordering::SeqCst),
252 "Cannot set time while clock is in realtime mode"
253 );
254
255 self.timestamp_ns.store(time.into(), Ordering::Release);
256
257 debug_assert!(
258 !self.realtime.load(Ordering::SeqCst),
259 "Invariant: clock must remain in static mode across `set_time`"
260 );
261 }
262
263 /// Increments the current (static-mode) time by `delta` nanoseconds and returns the updated value.
264 ///
265 /// Internally this uses [`AtomicU64::try_update`] with [`Ordering::AcqRel`] to ensure the increment is
266 /// atomic and visible to readers using `Acquire` loads.
267 ///
268 /// # Errors
269 ///
270 /// Returns an error if the increment would overflow `u64::MAX` or if called
271 /// while the clock is in real-time mode.
272 ///
273 /// # Thread Safety
274 ///
275 /// The mode check is not atomic with the subsequent update. If another thread calls
276 /// `make_realtime()` between the check and update, the invariant can be violated.
277 /// This is intentional: mode switching is a setup-time operation and should not
278 /// occur concurrently with time operations. Callers must ensure mode switches are
279 /// complete before resuming time operations.
280 pub fn increment_time(&self, delta: u64) -> anyhow::Result<UnixNanos> {
281 anyhow::ensure!(
282 !self.realtime.load(Ordering::SeqCst),
283 "Cannot increment time while clock is in realtime mode"
284 );
285
286 let previous =
287 match self
288 .timestamp_ns
289 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
290 current.checked_add(delta)
291 }) {
292 Ok(prev) => prev,
293 Err(_) => anyhow::bail!("Cannot increment time beyond u64::MAX"),
294 };
295
296 debug_assert!(
297 !self.realtime.load(Ordering::SeqCst),
298 "Invariant: clock must remain in static mode across `increment_time`"
299 );
300
301 Ok(UnixNanos::from(previous + delta))
302 }
303
304 /// Retrieves and updates the current "real-time" clock, returning a strictly increasing
305 /// timestamp based on system time.
306 ///
307 /// Internally:
308 /// - We fetch `now` from [`SystemTime::now()`].
309 /// - We do an atomic compare-and-exchange (using [`Ordering::AcqRel`]) to ensure the stored
310 /// timestamp is never less than the last timestamp.
311 ///
312 /// This ensures:
313 /// 1. **Monotonic increments**: The returned timestamp is strictly greater than the previous
314 /// one (by at least 1 nanosecond).
315 /// 2. **No backward jumps**: If the OS time moves backward, we ignore that shift to preserve
316 /// monotonicity.
317 /// 3. **Visibility**: In a multi-threaded environment, other threads see the updated value
318 /// once this compare-and-exchange completes.
319 ///
320 /// # Panics
321 ///
322 /// Panics if the internal counter has reached `u64::MAX`, which would indicate the process has
323 /// been running for longer than the representable range (~584 years) *or* the clock was
324 /// manually corrupted.
325 pub fn time_since_epoch(&self) -> UnixNanos {
326 // This method guarantees strict consistency but may incur a performance cost under
327 // high contention due to retries in the `compare_exchange` loop.
328 let now = nanos_since_unix_epoch();
329
330 loop {
331 // Acquire to observe the latest stored value
332 let last = self.timestamp_ns.load(Ordering::Acquire);
333
334 // Ensure we never wrap past u64::MAX - treat that as a fatal error
335 let incremented = last
336 .checked_add(1)
337 .expect("AtomicTime overflow: reached u64::MAX");
338 let next = now.max(incremented);
339
340 // AcqRel on success ensures this new value is published,
341 // Acquire on failure reloads if we lost a CAS race.
342 //
343 // Note that under heavy contention (many threads calling this in tight loops),
344 // the CAS loop may increase latency.
345 //
346 // However, in practice, the loop terminates quickly because:
347 // - System time naturally advances between iterations
348 // - Each iteration increments time by at least 1ns, preventing ABA problems
349 // - True contention requiring retry is rare in normal usage patterns
350 //
351 // The concurrent stress test (4 threads × 100k iterations) validates this approach.
352 if self
353 .timestamp_ns
354 .compare_exchange(last, next, Ordering::AcqRel, Ordering::Acquire)
355 .is_ok()
356 {
357 debug_assert!(
358 next > last,
359 "Invariant: time is strictly monotonic across CAS"
360 );
361 return UnixNanos::from(next);
362 }
363 }
364 }
365
366 /// Switches the clock to **real-time mode** (`realtime = true`).
367 ///
368 /// If transitioning from static mode, the internal counter is reset to the current
369 /// wall-clock time so that [`AtomicTime::time_since_epoch`] does not carry forward a
370 /// timestamp set during static mode (e.g. a backtest far in the future).
371 ///
372 /// Uses [`Ordering::SeqCst`] for the mode flag to ensure global ordering.
373 ///
374 /// # Thread Safety
375 ///
376 /// The mode swap and the counter reset are two separate atomic operations. A thread
377 /// reading between them can observe real-time mode with the stale static-mode counter
378 /// and return a timestamp derived from it (potentially far in the future), after which
379 /// the reset moves the clock backwards. Mode switching is a setup-time operation and
380 /// must not run concurrently with time reads.
381 pub fn make_realtime(&self) {
382 if !self.realtime.swap(true, Ordering::SeqCst) {
383 self.timestamp_ns
384 .store(nanos_since_unix_epoch(), Ordering::Release);
385 }
386 }
387
388 /// Switches the clock to **static mode** (`realtime = false`).
389 ///
390 /// If transitioning from real-time mode, the internal counter is snapshotted to the
391 /// current wall-clock time so that subsequent static reads return a reasonable value
392 /// rather than a stale or zero placeholder.
393 ///
394 /// Uses [`Ordering::SeqCst`] for the mode flag to ensure global ordering.
395 ///
396 /// # Thread Safety
397 ///
398 /// The mode swap and the counter snapshot are two separate atomic operations; see
399 /// [`AtomicTime::make_realtime`] for the race this implies. Mode switching is a
400 /// setup-time operation and must not run concurrently with time reads.
401 pub fn make_static(&self) {
402 if self.realtime.swap(false, Ordering::SeqCst) {
403 self.timestamp_ns
404 .store(nanos_since_unix_epoch(), Ordering::Release);
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use std::sync::Arc;
412
413 use rstest::*;
414
415 use super::*;
416
417 #[rstest]
418 fn test_global_clocks_initialization() {
419 let realtime_clock = get_atomic_clock_realtime();
420 assert!(realtime_clock.get_time_ns().as_u64() > 0);
421
422 let static_clock = get_atomic_clock_static();
423 static_clock.set_time(UnixNanos::from(500_000_000)); // 500 ms
424 assert_eq!(static_clock.get_time_ns().as_u64(), 500_000_000);
425 }
426
427 #[rstest]
428 fn test_mode_switching() {
429 let time = AtomicTime::new(true, UnixNanos::default());
430
431 // Verify real-time mode
432 let realtime_ns = time.get_time_ns();
433 assert!(realtime_ns.as_u64() > 0);
434
435 // Switch to static mode
436 time.make_static();
437 time.set_time(UnixNanos::from(1_000_000_000)); // 1 second
438 let static_ns = time.get_time_ns();
439 assert_eq!(static_ns.as_u64(), 1_000_000_000);
440
441 // Switch back to real-time mode
442 time.make_realtime();
443 let new_realtime_ns = time.get_time_ns();
444 assert!(new_realtime_ns.as_u64() > static_ns.as_u64());
445 }
446
447 #[rstest]
448 #[should_panic(expected = "Cannot set time while clock is in realtime mode")]
449 fn test_set_time_panics_in_realtime_mode() {
450 let clock = AtomicTime::new(true, UnixNanos::default());
451 clock.set_time(UnixNanos::from(123));
452 }
453
454 #[rstest]
455 fn test_increment_time_returns_error_in_realtime_mode() {
456 let clock = AtomicTime::new(true, UnixNanos::default());
457 let result = clock.increment_time(1);
458 assert!(result.is_err());
459 assert!(
460 result
461 .unwrap_err()
462 .to_string()
463 .contains("Cannot increment time while clock is in realtime mode")
464 );
465 }
466
467 #[rstest]
468 #[should_panic(expected = "AtomicTime overflow")]
469 fn test_time_since_epoch_overflow_panics() {
470 use std::sync::atomic::{AtomicBool, AtomicU64};
471
472 // Manually construct a clock with the counter already at u64::MAX
473 let clock = AtomicTime {
474 realtime: AtomicBool::new(true),
475 timestamp_ns: AtomicU64::new(u64::MAX),
476 };
477
478 // This call will attempt to add 1 and must panic
479 let _ = clock.time_since_epoch();
480 }
481
482 #[rstest]
483 fn test_new_realtime_ignores_initial_time() {
484 let before = nanos_since_unix_epoch();
485 let clock = AtomicTime::new(true, UnixNanos::from(u64::MAX));
486 let timestamp = clock.get_time_ns().as_u64();
487 let after = nanos_since_unix_epoch();
488
489 assert!(timestamp >= before);
490 assert!(timestamp <= after);
491 }
492
493 #[rstest]
494 fn test_make_static_snapshots_wall_time() {
495 // A fresh realtime clock that has never been read starts with timestamp_ns = 0.
496 // Switching to static should snapshot wall time, not leave it at 0.
497 let clock = AtomicTime::new(true, UnixNanos::default());
498 clock.make_static();
499 let ts = clock.get_time_ns();
500 assert!(
501 ts.as_u64() > 1_650_000_000_000_000_000,
502 "Expected wall-clock snapshot, was {ts}"
503 );
504 }
505
506 #[rstest]
507 fn test_make_realtime_resets_future_timestamp() {
508 // If static mode set the clock into the future, switching to realtime
509 // should reset to wall time so timestamps are not poisoned.
510 let clock = AtomicTime::new(false, UnixNanos::from(u64::MAX - 1_000));
511 clock.make_realtime();
512 let ts = clock.get_time_ns();
513 // Should be near current wall time, not near u64::MAX
514 let now = nanos_since_unix_epoch();
515 assert!(
516 ts.as_u64() <= now + 1_000_000_000, // within 1 second
517 "Expected wall-clock time, was {ts} (now={now})"
518 );
519 }
520
521 #[rstest]
522 fn test_make_static_idempotent() {
523 // Calling make_static on an already-static clock should not change the time
524 let clock = AtomicTime::new(false, UnixNanos::from(42));
525 clock.make_static();
526 assert_eq!(clock.get_time_ns(), UnixNanos::from(42));
527 }
528
529 #[rstest]
530 fn test_make_realtime_idempotent() {
531 // Calling make_realtime on an already-realtime clock should not reset the counter
532 let clock = AtomicTime::new(true, UnixNanos::default());
533 let ts1 = clock.get_time_ns();
534 clock.make_realtime(); // already realtime, should be a no-op
535 let ts2 = clock.get_time_ns();
536 assert!(ts2 >= ts1);
537 }
538
539 #[rstest]
540 fn test_static_time_is_stable() {
541 // Create a clock in static mode with an initial value
542 let clock = AtomicTime::new(false, UnixNanos::from(42));
543 let time1 = clock.get_time_ns();
544
545 // Sleep a bit to give the system time to change, if the clock were using real-time
546 std::thread::sleep(std::time::Duration::from_millis(10));
547 let time2 = clock.get_time_ns();
548
549 // In static mode, the value should remain unchanged
550 assert_eq!(time1, time2);
551 }
552
553 #[rstest]
554 fn test_increment_time() {
555 // Start in static mode
556 let time = AtomicTime::new(false, UnixNanos::from(0));
557
558 let updated_time = time.increment_time(500).unwrap();
559 assert_eq!(updated_time.as_u64(), 500);
560
561 let updated_time = time.increment_time(1_000).unwrap();
562 assert_eq!(updated_time.as_u64(), 1_500);
563 }
564
565 #[rstest]
566 fn test_increment_time_overflow_errors() {
567 let time = AtomicTime::new(false, UnixNanos::from(u64::MAX - 5));
568
569 let err = time.increment_time(10).unwrap_err();
570 assert_eq!(err.to_string(), "Cannot increment time beyond u64::MAX");
571 }
572
573 #[rstest]
574 fn test_increment_time_after_make_static() {
575 // Switching from realtime snapshots wall time; increments build on the snapshot
576 let clock = AtomicTime::new(true, UnixNanos::default());
577 clock.make_static();
578 let before = clock.get_time_ns();
579 let after = clock.increment_time(1_000).unwrap();
580 assert_eq!(after, before + 1_000_u64);
581 assert_eq!(clock.get_time_ns(), after);
582 }
583
584 #[rstest]
585 fn test_nanos_since_unix_epoch_vs_system_time() {
586 let unix_nanos = nanos_since_unix_epoch();
587 let system_ns = u64::try_from(duration_since_unix_epoch().as_nanos()).unwrap();
588 assert!(unix_nanos.abs_diff(system_ns) < NANOSECONDS_IN_SECOND);
589 }
590
591 #[rstest]
592 fn test_time_since_epoch_monotonicity() {
593 let clock = get_atomic_clock_realtime();
594 let mut previous = clock.time_since_epoch();
595 for _ in 0..1_000_000 {
596 let current = clock.time_since_epoch();
597 assert!(current > previous);
598 previous = current;
599 }
600 }
601
602 #[rstest]
603 fn test_time_since_epoch_strictly_increasing_concurrent() {
604 let time = Arc::new(AtomicTime::new(true, UnixNanos::default()));
605 let num_threads = 4;
606 let iterations = 100_000;
607 let mut handles = Vec::with_capacity(num_threads);
608
609 for thread_id in 0..num_threads {
610 let time_clone = Arc::clone(&time);
611
612 let handle = std::thread::spawn(move || {
613 let mut previous = time_clone.time_since_epoch().as_u64();
614
615 for i in 0..iterations {
616 let current = time_clone.time_since_epoch().as_u64();
617 assert!(
618 current > previous,
619 "Thread {thread_id}: iteration {i}: time did not increase: previous={previous}, current={current}",
620 );
621 previous = current;
622 }
623 });
624
625 handles.push(handle);
626 }
627
628 for handle in handles {
629 handle.join().unwrap();
630 }
631 }
632
633 #[rstest]
634 fn test_duration_since_unix_epoch() {
635 let time = AtomicTime::new(true, UnixNanos::default());
636 let duration = Duration::from_nanos(time.get_time_ns().into());
637 let now = SystemTime::now();
638
639 // Check if the duration is close to the actual difference between now and UNIX_EPOCH
640 let delta = now
641 .duration_since(UNIX_EPOCH)
642 .unwrap()
643 .checked_sub(duration);
644 assert!(delta.unwrap_or_default() < Duration::from_millis(100));
645
646 // Check if the duration is greater than a certain value (assuming the test is run after that point)
647 assert!(duration > Duration::from_mins(27_500_000));
648 }
649
650 #[rstest]
651 fn test_unix_timestamp_is_monotonic_increasing() {
652 let time = AtomicTime::new(true, UnixNanos::default());
653 let result1 = time.get_time();
654 let result2 = time.get_time();
655 let result3 = time.get_time();
656 let result4 = time.get_time();
657 let result5 = time.get_time();
658
659 assert!(result2 >= result1);
660 assert!(result3 >= result2);
661 assert!(result4 >= result3);
662 assert!(result5 >= result4);
663 assert!(result1 > 1_650_000_000.0);
664 }
665
666 #[rstest]
667 fn test_unix_timestamp_ms_is_monotonic_increasing() {
668 let time = AtomicTime::new(true, UnixNanos::default());
669 let result1 = time.get_time_ms();
670 let result2 = time.get_time_ms();
671 let result3 = time.get_time_ms();
672 let result4 = time.get_time_ms();
673 let result5 = time.get_time_ms();
674
675 assert!(result2 >= result1);
676 assert!(result3 >= result2);
677 assert!(result4 >= result3);
678 assert!(result5 >= result4);
679 assert!(result1 >= 1_650_000_000_000);
680 }
681
682 #[rstest]
683 fn test_unix_timestamp_us_is_monotonic_increasing() {
684 let time = AtomicTime::new(true, UnixNanos::default());
685 let result1 = time.get_time_us();
686 let result2 = time.get_time_us();
687 let result3 = time.get_time_us();
688 let result4 = time.get_time_us();
689 let result5 = time.get_time_us();
690
691 assert!(result2 >= result1);
692 assert!(result3 >= result2);
693 assert!(result4 >= result3);
694 assert!(result5 >= result4);
695 assert!(result1 > 1_650_000_000_000_000);
696 }
697
698 #[rstest]
699 fn test_unix_timestamp_ns_is_monotonic_increasing() {
700 let time = AtomicTime::new(true, UnixNanos::default());
701 let result1 = time.get_time_ns();
702 let result2 = time.get_time_ns();
703 let result3 = time.get_time_ns();
704 let result4 = time.get_time_ns();
705 let result5 = time.get_time_ns();
706
707 assert!(result2 >= result1);
708 assert!(result3 >= result2);
709 assert!(result4 >= result3);
710 assert!(result5 >= result4);
711 assert!(result1.as_u64() > 1_650_000_000_000_000_000);
712 }
713
714 #[rstest]
715 fn test_acquire_release_contract_static_mode() {
716 // This test explicitly proves the Acquire/Release memory ordering contract:
717 // - Writer thread uses set_time() which does Release store (see AtomicTime::set_time)
718 // - Reader thread uses get_time_ns() which does Acquire load (see AtomicTime::get_time_ns)
719 // - The Release-Acquire pair ensures all writes before Release are visible after Acquire
720
721 let clock = Arc::new(AtomicTime::new(false, UnixNanos::from(0)));
722 let aux_data = Arc::new(AtomicU64::new(0));
723 let done = Arc::new(AtomicBool::new(false));
724
725 // Writer thread: updates auxiliary data, then releases via set_time
726 let writer_clock = Arc::clone(&clock);
727 let writer_aux = Arc::clone(&aux_data);
728 let writer_done = Arc::clone(&done);
729
730 let writer = std::thread::spawn(move || {
731 for i in 1..=1_000u64 {
732 writer_aux.store(i, Ordering::Relaxed);
733
734 // Release store via set_time creates a release fence - all prior writes (including aux_data)
735 // must be visible to any thread that observes this time value via Acquire load
736 writer_clock.set_time(UnixNanos::from(i * 1000));
737
738 // Yield to encourage interleaving
739 std::thread::yield_now();
740 }
741 writer_done.store(true, Ordering::Release);
742 });
743
744 // Reader thread: acquires via get_time_ns, then checks auxiliary data
745 let reader_clock = Arc::clone(&clock);
746 let reader_aux = Arc::clone(&aux_data);
747 let reader_done = Arc::clone(&done);
748
749 let reader = std::thread::spawn(move || {
750 let mut last_time = 0u64;
751 let mut max_aux_seen = 0u64;
752
753 // Poll until writer is done, with no iteration limit
754 while !reader_done.load(Ordering::Acquire) {
755 let current_time = reader_clock.get_time_ns().as_u64();
756
757 if current_time > last_time {
758 // The Acquire in get_time_ns synchronizes with the Release in set_time,
759 // making aux_data visible
760 let aux_value = reader_aux.load(Ordering::Relaxed);
761
762 // Invariant: aux_value must never go backwards (proves Release-Acquire sync works)
763 if aux_value > 0 {
764 assert!(
765 aux_value >= max_aux_seen,
766 "Acquire/Release contract violated: aux went backwards from {max_aux_seen} to {aux_value}"
767 );
768 max_aux_seen = aux_value;
769 }
770
771 last_time = current_time;
772 }
773
774 std::thread::yield_now();
775 }
776
777 // Check final state after writer completes to ensure we observe updates
778 let final_time = reader_clock.get_time_ns().as_u64();
779 if final_time > last_time {
780 let final_aux = reader_aux.load(Ordering::Relaxed);
781 if final_aux > 0 {
782 assert!(
783 final_aux >= max_aux_seen,
784 "Acquire/Release contract violated: final aux {final_aux} < max {max_aux_seen}"
785 );
786 max_aux_seen = final_aux;
787 }
788 }
789
790 max_aux_seen
791 });
792
793 writer.join().unwrap();
794 let max_observed = reader.join().unwrap();
795
796 // Ensure the reader actually observed updates (not vacuously satisfied)
797 assert!(max_observed > 0, "Reader must observe writer updates");
798 }
799
800 #[rstest]
801 fn test_acquire_release_contract_increment_time() {
802 // Similar test for increment_time, which uses try_update with AcqRel (see AtomicTime::increment_time)
803
804 let clock = Arc::new(AtomicTime::new(false, UnixNanos::from(0)));
805 let aux_data = Arc::new(AtomicU64::new(0));
806 let done = Arc::new(AtomicBool::new(false));
807
808 let writer_clock = Arc::clone(&clock);
809 let writer_aux = Arc::clone(&aux_data);
810 let writer_done = Arc::clone(&done);
811
812 let writer = std::thread::spawn(move || {
813 for i in 1..=1_000u64 {
814 writer_aux.store(i, Ordering::Relaxed);
815 let _ = writer_clock.increment_time(1000).unwrap();
816 std::thread::yield_now();
817 }
818 writer_done.store(true, Ordering::Release);
819 });
820
821 let reader_clock = Arc::clone(&clock);
822 let reader_aux = Arc::clone(&aux_data);
823 let reader_done = Arc::clone(&done);
824
825 let reader = std::thread::spawn(move || {
826 let mut last_time = 0u64;
827 let mut max_aux = 0u64;
828
829 // Poll until writer is done, with no iteration limit
830 while !reader_done.load(Ordering::Acquire) {
831 let current_time = reader_clock.get_time_ns().as_u64();
832
833 if current_time > last_time {
834 let aux_value = reader_aux.load(Ordering::Relaxed);
835
836 // Invariant: aux_value must never regress (proves AcqRel sync works)
837 if aux_value > 0 {
838 assert!(
839 aux_value >= max_aux,
840 "AcqRel contract violated: aux regressed from {max_aux} to {aux_value}"
841 );
842 max_aux = aux_value;
843 }
844
845 last_time = current_time;
846 }
847
848 std::thread::yield_now();
849 }
850
851 // Check final state after writer completes to ensure we observe updates
852 let final_time = reader_clock.get_time_ns().as_u64();
853 if final_time > last_time {
854 let final_aux = reader_aux.load(Ordering::Relaxed);
855 if final_aux > 0 {
856 assert!(
857 final_aux >= max_aux,
858 "AcqRel contract violated: final aux {final_aux} < max {max_aux}"
859 );
860 max_aux = final_aux;
861 }
862 }
863
864 max_aux
865 });
866
867 writer.join().unwrap();
868 let max_observed = reader.join().unwrap();
869
870 // Ensure the reader actually observed updates (not vacuously satisfied)
871 assert!(max_observed > 0, "Reader must observe writer updates");
872 }
873
874 // The wall-clock seam (`wall_clock_now`) routes through madsim's virtual
875 // clock under simulation. Sleeping for 60 virtual seconds must advance
876 // the value returned by `nanos_since_unix_epoch` by 60s in wall-clock
877 // terms. If the cfg gate fell through to `SystemTime::now()`, the elapsed
878 // value would only reflect real wall-clock time (~0ms) and the assertion
879 // would fail.
880 #[cfg(all(feature = "simulation", madsim))]
881 #[madsim::test]
882 async fn test_wall_clock_advances_with_virtual_time() {
883 let before = nanos_since_unix_epoch();
884 madsim::time::sleep(std::time::Duration::from_mins(1)).await;
885 let after = nanos_since_unix_epoch();
886
887 let elapsed_ns = after.saturating_sub(before);
888 let sixty_seconds_ns = 60 * NANOSECONDS_IN_SECOND;
889 assert!(
890 elapsed_ns >= sixty_seconds_ns,
891 "wall clock did not advance by full virtual sleep: elapsed={elapsed_ns}ns"
892 );
893 }
894}