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}