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}