lsm_tree/time.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5use core::time::Duration;
6
7/// A source of wall-clock time.
8///
9/// The engine reads wall-clock time (for the `created_at` stamp on tables and
10/// blob files, and for FIFO TTL expiry) through this trait. A tree reads the
11/// clock its config names ([`Config::with_clock`](crate::Config::with_clock)),
12/// and the process clock without one: under `std` the built-in
13/// [`SystemClock`], under `no_std`, where there is no ambient system clock,
14/// the one a consumer (e.g. a WASM host exposing `Date.now()`) registers once
15/// via `set_clock` before opening a tree.
16///
17/// # Examples
18///
19/// ```
20/// # use lsm_tree::Clock;
21/// # use core::time::Duration;
22/// struct FixedClock(Duration);
23/// impl Clock for FixedClock {
24/// fn unix_time(&self) -> Duration {
25/// self.0
26/// }
27/// }
28/// let clock = FixedClock(Duration::from_secs(1_700_000_000));
29/// assert_eq!(clock.unix_time().as_secs(), 1_700_000_000);
30/// ```
31#[diagnostic::on_unimplemented(
32 message = "`{Self}` is not a wall-clock source",
33 label = "this type does not implement `Clock`",
34 note = "implement `Clock` to inject a wall-clock under `no_std`, or enable the `std` feature to use the built-in `SystemClock`"
35)]
36pub trait Clock: Send + Sync {
37 /// Wall-clock time elapsed since the Unix epoch.
38 ///
39 /// A clock with no real time source should return [`Duration::ZERO`] (the
40 /// epoch), which disables TTL expiry rather than expiring everything.
41 fn unix_time(&self) -> Duration;
42}
43
44/// The system wall-clock, backed by [`std::time::SystemTime`].
45///
46/// The default [`Clock`] under `feature = "std"`; consumers never need to
47/// register it explicitly.
48#[cfg(feature = "std")]
49#[derive(Debug, Clone, Copy, Default)]
50pub struct SystemClock;
51
52#[cfg(feature = "std")]
53impl Clock for SystemClock {
54 fn unix_time(&self) -> Duration {
55 #[expect(
56 clippy::expect_used,
57 reason = "the system clock predates the Unix epoch only on a grossly misconfigured host"
58 )]
59 std::time::SystemTime::now()
60 .duration_since(std::time::SystemTime::UNIX_EPOCH)
61 .expect("system time is before the Unix epoch")
62 }
63}
64
65/// Gets the unix timestamp as a duration (wall-clock time since the epoch).
66///
67/// Reads through the active [`Clock`]: the built-in [`SystemClock`] under
68/// `std`, or the caller-registered clock under `no_std` (see `set_clock`).
69/// Until a `no_std` clock is registered the value is [`Duration::ZERO`]
70/// (epoch), which disables TTL expiry rather than expiring everything.
71pub fn unix_timestamp() -> Duration {
72 #[cfg(test)]
73 #[allow(clippy::significant_drop_in_scrutinee, clippy::expect_used)]
74 {
75 if let Some(cell) = NOW_OVERRIDE.get()
76 && let Some(override_val) = *cell.lock().expect("lock is poisoned")
77 {
78 return override_val;
79 }
80 }
81
82 #[cfg(feature = "std")]
83 {
84 SystemClock.unix_time()
85 }
86
87 #[cfg(not(feature = "std"))]
88 {
89 nostd_clock::now()
90 }
91}
92
93/// The time on `clock`, a tree's own clock, or on the process clock
94/// ([`unix_timestamp`]) when the tree has none.
95pub fn now_on(clock: Option<&alloc::sync::Arc<dyn Clock>>) -> Duration {
96 clock.map_or_else(unix_timestamp, |clock| clock.unix_time())
97}
98
99/// Monotonic instant used for elapsed-time logging on the compaction / flush
100/// paths.
101///
102/// Under `std` this is a re-export of `std::time::Instant`. Under `no_std`
103/// there is no ambient monotonic clock, so this is a zero-sized stub whose
104/// `elapsed` always reports `core::time::Duration::ZERO`
105/// — the timing logs degrade to `0ns` rather than failing to compile.
106// no-std: wire a caller-provided monotonic Clock hook (mirroring the
107// `unix_timestamp` wall-clock hook) if real elapsed timing is needed.
108#[cfg(feature = "std")]
109pub use std::time::Instant;
110
111/// See the `std` variant — a no-op monotonic-instant stub for `no_std`.
112#[cfg(not(feature = "std"))]
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct Instant;
115
116#[cfg(not(feature = "std"))]
117impl Instant {
118 /// Returns the (stub) current instant.
119 #[must_use]
120 pub const fn now() -> Self {
121 Self
122 }
123
124 /// Always [`core::time::Duration::ZERO`] under `no_std` (no monotonic clock).
125 #[must_use]
126 pub const fn elapsed(&self) -> core::time::Duration {
127 core::time::Duration::ZERO
128 }
129}
130
131/// Registers the [`Clock`] used by [`unix_timestamp`] under `no_std` (e.g. a
132/// WASM host's `Date.now()`). Idempotent: the first registration wins, later
133/// calls are ignored. Only present under `no_std`: under `std` the built-in
134/// [`SystemClock`] is always available, so no registration is needed.
135#[cfg(not(feature = "std"))]
136pub fn set_clock(clock: alloc::boxed::Box<dyn Clock>) {
137 nostd_clock::set(clock);
138}
139
140#[cfg(not(feature = "std"))]
141mod nostd_clock {
142 use super::Clock;
143 use alloc::boxed::Box;
144 use core::time::Duration;
145 use once_cell::race::OnceBox;
146
147 // Caller-injected wall-clock. Lock-free (atomic pointer), set once.
148 static CLOCK: OnceBox<Box<dyn Clock>> = OnceBox::new();
149
150 pub fn set(clock: Box<dyn Clock>) {
151 let _ = CLOCK.set(Box::new(clock));
152 }
153
154 pub fn now() -> Duration {
155 CLOCK
156 .get()
157 .map_or(Duration::ZERO, |clock| clock.unix_time())
158 }
159}
160
161#[cfg(test)]
162use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
163
164#[cfg(test)]
165static NOW_OVERRIDE: OnceLock<Mutex<Option<std::time::Duration>>> = OnceLock::new();
166
167/// Held for a whole test that overrides the clock, so two such tests in one
168/// process never see each other's override.
169#[cfg(test)]
170static CLOCK_OWNER: Mutex<()> = Mutex::new(());
171
172#[cfg(test)]
173#[allow(clippy::expect_used)]
174fn set_unix_timestamp_for_test(value: Option<std::time::Duration>) {
175 let cell = NOW_OVERRIDE.get_or_init(|| Mutex::new(None));
176 *cell.lock().expect("lock is poisoned") = value;
177}
178
179/// Handle to the test clock override, lent by [`with_test_clock`].
180#[cfg(test)]
181pub struct TestClock {
182 _lent: (),
183}
184
185#[cfg(test)]
186impl TestClock {
187 /// Pins [`unix_timestamp`] at `secs` since the epoch.
188 #[expect(
189 clippy::unused_self,
190 reason = "the receiver proves the caller holds the clock"
191 )]
192 pub fn set_secs(&self, secs: u64) {
193 set_unix_timestamp_for_test(Some(Duration::from_secs(secs)));
194 }
195}
196
197/// The clock lock, held for one [`with_test_clock`] call; dropping it clears
198/// the override.
199#[cfg(test)]
200struct HeldClock {
201 _owner: MutexGuard<'static, ()>,
202}
203
204#[cfg(test)]
205impl Drop for HeldClock {
206 fn drop(&mut self) {
207 set_unix_timestamp_for_test(None);
208 }
209}
210
211/// Runs `f` with exclusive use of the test clock override.
212///
213/// The override is process-wide, and `cargo test` runs a crate's tests as
214/// threads of one process, so every test that sets it, and every test whose
215/// result depends on the clock it reads, runs inside this call: other clock
216/// tests wait until it returns, and the override starts and ends unset, even
217/// when `f` returns early or panics. A test that only reads leaves it unset
218/// and sees the real clock.
219#[cfg(test)]
220pub fn with_test_clock<R>(f: impl FnOnce(&TestClock) -> R) -> R {
221 // A test that panicked while holding the clock poisons the lock; the
222 // override it left is cleared below, so the lock is still usable.
223 let held = HeldClock {
224 _owner: CLOCK_OWNER.lock().unwrap_or_else(PoisonError::into_inner),
225 };
226 set_unix_timestamp_for_test(None);
227 let result = f(&TestClock { _lent: () });
228 drop(held);
229 result
230}
231
232#[cfg(test)]
233mod tests;