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;