entelix_core/time.rs
1//! `Clock` — monotonic-clock abstraction shared across the
2//! workspace. Living in `entelix-core` so any sub-crate that needs
3//! a time source (rate limiters, retry backoff, cost-rate windows,
4//! TTL pruning) can take a `&dyn Clock` without depending on
5//! `entelix-policy`.
6//!
7//! Production code wires [`SystemClock`] (delegates to
8//! `tokio::time::Instant`); tests pass a deterministic clock so
9//! `tokio::time::pause()` + manual `advance` make consumers walk a
10//! known schedule.
11
12/// Monotonic-clock abstraction. Implementors must produce strictly
13/// non-decreasing values; jitter or skew breaks any consumer that
14/// computes elapsed time from the difference of two reads
15/// (rate-limit buckets, exponential backoff, TTL windows).
16pub trait Clock: Send + Sync + 'static {
17 /// Microseconds since some fixed origin. The origin doesn't
18 /// matter — only differences are read by consumers.
19 fn now_micros(&self) -> u64;
20}
21
22/// `Clock` backed by `tokio::time::Instant`. Honours
23/// `tokio::time::pause` so test harnesses can simulate elapsed
24/// time without real waits.
25#[derive(Clone, Copy, Debug, Default)]
26pub struct SystemClock;
27
28impl Clock for SystemClock {
29 fn now_micros(&self) -> u64 {
30 // `tokio::time::Instant` uses a monotonic source; converting
31 // through a stable origin keeps the absolute value bounded
32 // for the process lifetime.
33 let origin = origin_instant();
34 let now = tokio::time::Instant::now();
35 u64::try_from(now.duration_since(origin).as_micros()).unwrap_or(u64::MAX)
36 }
37}
38
39fn origin_instant() -> tokio::time::Instant {
40 use std::sync::OnceLock;
41 static ORIGIN: OnceLock<tokio::time::Instant> = OnceLock::new();
42 *ORIGIN.get_or_init(tokio::time::Instant::now)
43}