saluki_common/time.rs
1//! Time-related functions.
2
3use std::{
4 sync::{
5 atomic::{AtomicU64, Ordering::Relaxed},
6 Once,
7 },
8 thread,
9 time::{Duration, SystemTime},
10};
11
12static COARSE_TIME_INITIALIZED: Once = Once::new();
13static COARSE_TIME: AtomicU64 = AtomicU64::new(0);
14const COARSE_TIME_UPDATE_INTERVAL: Duration = Duration::from_millis(250);
15
16/// Get the current Unix timestamp, in seconds.
17///
18/// This function is accurate, as it always retrieves the current time for each call. In scenarios where this function
19/// is being called frequently, it may pose an unacceptable performance overhead. In such cases, consider using
20/// `get_coarse_unix_timestamp`, which provides a cached value that's updated periodically.
21pub fn get_unix_timestamp() -> u64 {
22 let since_unix_epoch = SystemTime::now()
23 .duration_since(SystemTime::UNIX_EPOCH)
24 .unwrap_or_default();
25 since_unix_epoch.as_secs()
26}
27
28/// Get the current coarse Unix timestamp, in seconds.
29///
30/// In scenarios where the current Unix timestamp is needed frequently, this function provides a cached value that's
31/// updated periodically. As the precision of the timestamp is one second, this function allows trading off accuracy
32/// (see below) for reduced overhead. The resulting value is considered "coarse," because it might be off by significant
33/// percentage of the overall precision.
34///
35/// # Accuracy
36///
37/// The underlying coarse time is updated roughly every 250 milliseconds, so the value returned by this function may be
38/// behind by up to 250 milliseconds. This means that if calling the function at true time `t` (where `t` is in
39/// seconds), the value returned may be `t-1` _or_ `t`, but will never be behind by more than 250 milliseconds, and
40/// never _ahead_ of `t`.
41pub fn get_coarse_unix_timestamp() -> u64 {
42 // Initialize a background thread to update the coarse time if it hasn't been initialized yet.
43 COARSE_TIME_INITIALIZED.call_once(|| {
44 // Initialize the coarse time with the current Unix timestamp.
45 COARSE_TIME.store(get_unix_timestamp(), Relaxed);
46
47 thread::spawn(|| {
48 loop {
49 // Sleep for 250 milliseconds.
50 thread::sleep(COARSE_TIME_UPDATE_INTERVAL);
51
52 // Update the coarse time with the current Unix timestamp.
53 COARSE_TIME.store(get_unix_timestamp(), Relaxed);
54 }
55 });
56 });
57
58 COARSE_TIME.load(Relaxed)
59}
60
61#[cfg(test)]
62mod tests {
63 use std::{thread::sleep, time::Duration};
64
65 use super::*;
66
67 #[test]
68 fn coarse_timestamp_never_exceeds_accurate_timestamp() {
69 // The documented contract is that the coarse timestamp is never _ahead_ of the true time:
70 // it is only ever equal to it or lagging behind it, because it is a cached snapshot of a
71 // previous `get_unix_timestamp()` call. Sample repeatedly to make an accidental "ahead"
72 // read unlikely to slip through.
73 for _ in 0..1_000 {
74 let coarse = get_coarse_unix_timestamp();
75 let accurate = get_unix_timestamp();
76 assert!(
77 coarse <= accurate,
78 "coarse timestamp {coarse} must never be ahead of accurate timestamp {accurate}"
79 );
80 }
81 }
82
83 #[test]
84 fn coarse_timestamp_is_monotonically_non_decreasing() {
85 // The coarse timestamp is only ever replaced with a newer `get_unix_timestamp()` value, so
86 // successive reads must never go backwards, including across an update-interval boundary.
87 let first = get_coarse_unix_timestamp();
88
89 let mut previous = first;
90 for _ in 0..1_000 {
91 let current = get_coarse_unix_timestamp();
92 assert!(
93 current >= previous,
94 "coarse timestamp went backwards: {previous} -> {current}"
95 );
96 previous = current;
97 }
98
99 // Sleep past a couple of update intervals so the background updater runs, then confirm the
100 // value has not regressed.
101 sleep(COARSE_TIME_UPDATE_INTERVAL * 3);
102 let after_sleep = get_coarse_unix_timestamp();
103 assert!(
104 after_sleep >= previous,
105 "coarse timestamp regressed after update: {previous} -> {after_sleep}"
106 );
107 }
108
109 #[test]
110 fn coarse_timestamp_tracks_accurate_within_staleness_bound() {
111 // The documented staleness bound is that the coarse value may lag true time by at most
112 // ~250ms, i.e. in whole seconds it is either `t` or `t-1`. Once the background updater is
113 // running, the difference against the accurate timestamp should therefore be no more than
114 // one second. Retry across a short window to absorb one-off scheduling jitter on busy CI.
115 get_coarse_unix_timestamp();
116 sleep(COARSE_TIME_UPDATE_INTERVAL * 2);
117
118 let mut within_bound = false;
119 for _ in 0..20 {
120 let accurate = get_unix_timestamp();
121 let coarse = get_coarse_unix_timestamp();
122 if accurate - coarse <= 1 {
123 within_bound = true;
124 break;
125 }
126 sleep(Duration::from_millis(100));
127 }
128
129 assert!(
130 within_bound,
131 "coarse timestamp never came within one second of the accurate timestamp"
132 );
133 }
134}