Skip to main content

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;