Skip to main content

decdn_node/
coarse_clock.rs

1//! A coarse wall clock read off the per-lane hot path (issue #1792 item 4).
2//!
3//! The voucher/preimage accept path reads the wall clock twice while it holds
4//! the per-lane [`Mutex`](tokio::sync::Mutex): the capability-expiry gate and
5//! the `last_voucher_at` liveness stamp. Both tolerate roughly one-second
6//! resolution — expiry is an on-chain grant deadline, and the stamp only drives
7//! the admin "seconds since last voucher" readout — so paying a
8//! `SystemTime::now()` syscall inside that critical section is pure per-proof
9//! overhead that lengthens the most contended lock hold on the paid path.
10//!
11//! [`CoarseClock`] turns each of those two syscalls into a single relaxed
12//! [`AtomicU64`] load (still two reads per proof, one load each). A background
13//! task ([`CoarseClock::spawn_refresher`]) stamps the current wall clock into
14//! the cell on a timer; the hot path just reads it.
15//!
16//! **Fallback.** Until a refresher runs the cell holds `0`, and every read falls
17//! back to a live `SystemTime::now()`. So a clock with no refresher attached
18//! (every unit and loopback test, which build a handler without one) reports the
19//! exact wall clock on every read — the coarsening is a production-only
20//! optimization, not a semantic change. `0` is a
21//! safe sentinel: a live millisecond reading is `0` only at the Unix epoch
22//! itself, which a running node's clock is not at, so a `0` in the cell means
23//! "unrefreshed", not "midnight 1970". Even a stray `0` (a refresher that wrote
24//! one, or a clock pinned there) is harmless — reads just route to a live
25//! `SystemTime::now()` until the next tick, correct but not coarse.
26
27use std::sync::Arc;
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::time::Duration;
30
31/// Live wall clock in milliseconds since the Unix epoch, or `0` if the system
32/// clock is before the epoch. The refresher writes this into the cell, and a
33/// cell still at its `0` sentinel falls back to it. Mirrors the handler's
34/// `unix_millis`; kept local so this module stays a self-contained utility with
35/// no dependency back into the handler.
36fn live_millis() -> u64 {
37    std::time::SystemTime::now()
38        .duration_since(std::time::UNIX_EPOCH)
39        .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
40}
41
42/// A wall clock the paid-delivery hot path can read with a single relaxed atomic
43/// load instead of a `SystemTime::now()` syscall (issue #1792 item 4).
44///
45/// Refreshed on a timer by [`Self::spawn_refresher`]; reads fall back to a live
46/// clock until (and if ever) a refresher runs. See the module docs.
47#[derive(Debug, Default)]
48pub struct CoarseClock {
49    /// Unix milliseconds as of the last refresher tick. `0` means "never
50    /// refreshed" and routes every read to [`live_millis`].
51    millis: AtomicU64,
52}
53
54impl CoarseClock {
55    /// A clock with no refresher: every read is live until [`Self::spawn_refresher`]
56    /// starts stamping the cell.
57    #[must_use]
58    pub const fn new() -> Self {
59        Self {
60            millis: AtomicU64::new(0),
61        }
62    }
63
64    /// Unix milliseconds — the refreshed value, or a live reading while the cell
65    /// still holds its `0` sentinel.
66    #[must_use]
67    pub fn unix_millis(&self) -> u64 {
68        match self.millis.load(Ordering::Relaxed) {
69            0 => live_millis(),
70            coarse => coarse,
71        }
72    }
73
74    /// Unix seconds, floored from [`Self::unix_millis`]. Matches
75    /// `SystemTime::now().duration_since(UNIX_EPOCH).as_secs()` for the same
76    /// instant, so it is a drop-in for the expiry gate's prior
77    /// `payment_settlement::unix_now()`.
78    #[must_use]
79    pub fn unix_seconds(&self) -> u64 {
80        self.unix_millis() / 1000
81    }
82
83    /// Stamp the current wall clock into the cell. Only [`Self::spawn_refresher`]
84    /// calls this.
85    fn tick(&self) {
86        self.millis.store(live_millis(), Ordering::Relaxed);
87    }
88
89    /// Spawn a background task that refreshes `clock` every `interval` until the
90    /// last strong reference to it is dropped.
91    ///
92    /// The task holds only a [`Weak`](std::sync::Weak), so it is
93    /// **self-terminating** and needs no [`StopHandle`](crate::stop_handle::StopHandle)
94    /// wiring: once every [`ClientHandler`](crate::handlers::client::ClientHandler)
95    /// holding the clock is gone (runtime shutdown), the next `upgrade()` fails
96    /// and the loop exits — at most one `interval` later. It also owns no queue
97    /// and awaits no sender, so the detached-task hazard `StopHandle` exists to
98    /// prevent does not apply here.
99    ///
100    /// The first tick fires immediately (a fresh [`tokio::time::interval`] is
101    /// ready at once), so the cell leaves its `0` sentinel as soon as the task is
102    /// scheduled; reads before then are live, not wrong.
103    pub fn spawn_refresher(clock: &Arc<Self>, interval: Duration) {
104        let weak = Arc::downgrade(clock);
105        tokio::spawn(async move {
106            let mut ticker = tokio::time::interval(interval);
107            // A refresh missed under load is stale, not owed — coalesce rather
108            // than fire a burst of catch-up ticks.
109            ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
110            loop {
111                ticker.tick().await;
112                let Some(clock) = weak.upgrade() else {
113                    break;
114                };
115                clock.tick();
116            }
117        });
118    }
119}
120
121#[cfg(test)]
122mod tests;