lsm_tree/time.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Structured World Foundation
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. Under `std` the
11/// built-in [`SystemClock`] is used automatically. Under `no_std` there is no
12/// ambient system clock, so a consumer (e.g. a WASM host exposing `Date.now()`)
13/// injects one once via `set_clock` before opening a tree.
14///
15/// # Examples
16///
17/// ```
18/// # use lsm_tree::Clock;
19/// # use core::time::Duration;
20/// struct FixedClock(Duration);
21/// impl Clock for FixedClock {
22/// fn unix_time(&self) -> Duration {
23/// self.0
24/// }
25/// }
26/// let clock = FixedClock(Duration::from_secs(1_700_000_000));
27/// assert_eq!(clock.unix_time().as_secs(), 1_700_000_000);
28/// ```
29#[diagnostic::on_unimplemented(
30 message = "`{Self}` is not a wall-clock source",
31 label = "this type does not implement `Clock`",
32 note = "implement `Clock` to inject a wall-clock under `no_std`, or enable the `std` feature to use the built-in `SystemClock`"
33)]
34pub trait Clock: Send + Sync {
35 /// Wall-clock time elapsed since the Unix epoch.
36 ///
37 /// A clock with no real time source should return [`Duration::ZERO`] (the
38 /// epoch), which disables TTL expiry rather than expiring everything.
39 fn unix_time(&self) -> Duration;
40}
41
42/// The system wall-clock, backed by [`std::time::SystemTime`].
43///
44/// The default [`Clock`] under `feature = "std"`; consumers never need to
45/// register it explicitly.
46#[cfg(feature = "std")]
47#[derive(Debug, Clone, Copy, Default)]
48pub struct SystemClock;
49
50#[cfg(feature = "std")]
51impl Clock for SystemClock {
52 fn unix_time(&self) -> Duration {
53 #[expect(
54 clippy::expect_used,
55 reason = "the system clock predates the Unix epoch only on a grossly misconfigured host"
56 )]
57 std::time::SystemTime::now()
58 .duration_since(std::time::SystemTime::UNIX_EPOCH)
59 .expect("system time is before the Unix epoch")
60 }
61}
62
63/// Gets the unix timestamp as a duration (wall-clock time since the epoch).
64///
65/// Reads through the active [`Clock`]: the built-in [`SystemClock`] under
66/// `std`, or the caller-registered clock under `no_std` (see `set_clock`).
67/// Until a `no_std` clock is registered the value is [`Duration::ZERO`]
68/// (epoch), which disables TTL expiry rather than expiring everything.
69pub fn unix_timestamp() -> Duration {
70 #[cfg(test)]
71 #[allow(clippy::significant_drop_in_scrutinee, clippy::expect_used)]
72 {
73 if let Some(cell) = NOW_OVERRIDE.get()
74 && let Some(override_val) = *cell.lock().expect("lock is poisoned")
75 {
76 return override_val;
77 }
78 }
79
80 #[cfg(feature = "std")]
81 {
82 SystemClock.unix_time()
83 }
84
85 #[cfg(not(feature = "std"))]
86 {
87 nostd_clock::now()
88 }
89}
90
91/// Monotonic instant used for elapsed-time logging on the compaction / flush
92/// paths.
93///
94/// Under `std` this is a re-export of `std::time::Instant`. Under `no_std`
95/// there is no ambient monotonic clock, so this is a zero-sized stub whose
96/// `elapsed` always reports `core::time::Duration::ZERO`
97/// — the timing logs degrade to `0ns` rather than failing to compile.
98// no-std: wire a caller-provided monotonic Clock hook (mirroring the
99// `unix_timestamp` wall-clock hook) if real elapsed timing is needed.
100#[cfg(feature = "std")]
101pub use std::time::Instant;
102
103/// See the `std` variant — a no-op monotonic-instant stub for `no_std`.
104#[cfg(not(feature = "std"))]
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct Instant;
107
108#[cfg(not(feature = "std"))]
109impl Instant {
110 /// Returns the (stub) current instant.
111 #[must_use]
112 pub const fn now() -> Self {
113 Self
114 }
115
116 /// Always [`core::time::Duration::ZERO`] under `no_std` (no monotonic clock).
117 #[must_use]
118 pub const fn elapsed(&self) -> core::time::Duration {
119 core::time::Duration::ZERO
120 }
121}
122
123/// Registers the [`Clock`] used by [`unix_timestamp`] under `no_std` (e.g. a
124/// WASM host's `Date.now()`). Idempotent: the first registration wins, later
125/// calls are ignored. Only present under `no_std`: under `std` the built-in
126/// [`SystemClock`] is always available, so no registration is needed.
127#[cfg(not(feature = "std"))]
128pub fn set_clock(clock: alloc::boxed::Box<dyn Clock>) {
129 nostd_clock::set(clock);
130}
131
132#[cfg(not(feature = "std"))]
133mod nostd_clock {
134 use super::Clock;
135 use alloc::boxed::Box;
136 use core::time::Duration;
137 use once_cell::race::OnceBox;
138
139 // Caller-injected wall-clock. Lock-free (atomic pointer), set once.
140 static CLOCK: OnceBox<Box<dyn Clock>> = OnceBox::new();
141
142 pub fn set(clock: Box<dyn Clock>) {
143 let _ = CLOCK.set(Box::new(clock));
144 }
145
146 pub fn now() -> Duration {
147 CLOCK
148 .get()
149 .map_or(Duration::ZERO, |clock| clock.unix_time())
150 }
151}
152
153#[cfg(test)]
154use std::sync::{Mutex, OnceLock};
155
156#[cfg(test)]
157static NOW_OVERRIDE: OnceLock<Mutex<Option<std::time::Duration>>> = OnceLock::new();
158
159#[cfg(test)]
160#[allow(clippy::expect_used)]
161pub fn set_unix_timestamp_for_test(value: Option<std::time::Duration>) {
162 let cell = NOW_OVERRIDE.get_or_init(|| Mutex::new(None));
163 *cell.lock().expect("lock is poisoned") = value;
164}
165
166#[cfg(test)]
167mod tests;