Skip to main content

ember_core/
time.rs

1//! Compact monotonic time utilities.
2//!
3//! Uses a process-local monotonic clock for timestamps that are smaller
4//! than std::time::Instant (8 bytes vs 16 bytes for Option<Instant>).
5//!
6//! All internal expiry values are stored as monotonic milliseconds since
7//! process start. Use `monotonic_to_unix_ms` to convert to wall-clock Unix
8//! timestamps for commands like EXPIRETIME and PEXPIRETIME.
9
10use std::sync::OnceLock;
11use std::time::{Instant, SystemTime, UNIX_EPOCH};
12
13/// Returns current monotonic time in milliseconds since process start.
14#[inline]
15pub fn now_ms() -> u64 {
16    static START: OnceLock<Instant> = OnceLock::new();
17    let start = START.get_or_init(Instant::now);
18    start.elapsed().as_millis() as u64
19}
20
21/// Returns current monotonic time in seconds since process start, as u32.
22///
23/// Used for LRU last-access tracking where millisecond precision isn't
24/// needed and the smaller type improves cache-line packing. Wraps after
25/// ~136 years — well beyond any realistic process lifetime.
26#[inline]
27pub fn now_secs() -> u32 {
28    static START: OnceLock<Instant> = OnceLock::new();
29    let start = START.get_or_init(Instant::now);
30    start.elapsed().as_secs() as u32
31}
32
33/// Anchors the monotonic clock to wall time once and caches it for all
34/// subsequent conversions. Both `monotonic_to_unix_ms` and
35/// `unix_ms_to_monotonic_ms` share this anchor so the two operations are
36/// perfectly invertible.
37struct ClockAnchor {
38    /// Unix epoch ms at the moment we captured the anchor.
39    unix_ms_at_capture: u64,
40    /// Monotonic ms at the moment we captured the anchor.
41    mono_ms_at_capture: u64,
42}
43
44fn clock_anchor() -> &'static ClockAnchor {
45    static ANCHOR: OnceLock<ClockAnchor> = OnceLock::new();
46    ANCHOR.get_or_init(|| {
47        let unix_ms_at_capture = SystemTime::now()
48            .duration_since(UNIX_EPOCH)
49            .unwrap_or_default()
50            .as_millis()
51            .min(u64::MAX as u128) as u64;
52        let mono_ms_at_capture = now_ms();
53        ClockAnchor {
54            unix_ms_at_capture,
55            mono_ms_at_capture,
56        }
57    })
58}
59
60/// Converts a monotonic expiry timestamp (ms since process start) to a Unix
61/// epoch timestamp in milliseconds.
62///
63/// The conversion anchors the monotonic clock to wall time on first call using
64/// a single `SystemTime::now()` sample. Subsequent calls use only the fast
65/// monotonic clock and arithmetic — no system call.
66///
67/// Returns `None` if `expires_at_ms` is `NO_EXPIRY`.
68#[inline]
69pub fn monotonic_to_unix_ms(expires_at_ms: u64) -> Option<u64> {
70    if expires_at_ms == NO_EXPIRY {
71        return None;
72    }
73    let anchor = clock_anchor();
74    // unix_ms = unix_at_capture + (mono_expiry - mono_at_capture)
75    let offset = expires_at_ms.saturating_sub(anchor.mono_ms_at_capture);
76    Some(anchor.unix_ms_at_capture.saturating_add(offset))
77}
78
79/// Converts a Unix epoch timestamp in milliseconds to a monotonic expiry
80/// value suitable for storage in `Entry::expires_at_ms`.
81///
82/// The inverse of `monotonic_to_unix_ms`. Used by EXPIREAT and PEXPIREAT
83/// to convert an absolute wall-clock timestamp into the internal monotonic
84/// representation. Both functions share the same clock anchor so the
85/// conversion is coherent.
86///
87/// A timestamp in the past results in a value less than or equal to
88/// `now_ms()`, which means the key will be treated as already expired on
89/// the next access.
90#[inline]
91pub fn unix_ms_to_monotonic_ms(unix_ms: u64) -> u64 {
92    let anchor = clock_anchor();
93    // mono_expiry = mono_at_capture + (unix_ms - unix_at_capture)
94    let offset = unix_ms.saturating_sub(anchor.unix_ms_at_capture);
95    anchor.mono_ms_at_capture.saturating_add(offset)
96}
97
98/// Sentinel value meaning "no expiry".
99pub const NO_EXPIRY: u64 = 0;
100
101/// Returns true if the given expiry timestamp has passed.
102#[inline]
103pub fn is_expired(expires_at_ms: u64) -> bool {
104    expires_at_ms != NO_EXPIRY && now_ms() >= expires_at_ms
105}
106
107/// Converts a Duration to an absolute expiry timestamp.
108#[inline]
109pub fn expiry_from_duration(ttl: Option<std::time::Duration>) -> u64 {
110    ttl.map(|d| {
111        let ms = d.as_millis().min(u64::MAX as u128) as u64;
112        now_ms().saturating_add(ms)
113    })
114    .unwrap_or(NO_EXPIRY)
115}
116
117/// Returns remaining TTL in seconds, or None if no expiry.
118#[inline]
119pub fn remaining_secs(expires_at_ms: u64) -> Option<u64> {
120    remaining(expires_at_ms, 1000)
121}
122
123/// Returns remaining TTL in milliseconds, or None if no expiry.
124#[inline]
125pub fn remaining_ms(expires_at_ms: u64) -> Option<u64> {
126    remaining(expires_at_ms, 1)
127}
128
129/// Shared implementation for remaining TTL with a unit divisor.
130#[inline]
131fn remaining(expires_at_ms: u64, divisor: u64) -> Option<u64> {
132    if expires_at_ms == NO_EXPIRY {
133        None
134    } else {
135        let now = now_ms();
136        Some(expires_at_ms.saturating_sub(now) / divisor)
137    }
138}