modkit/telemetry/throttled_log.rs
1//! Lock-free throttled logging helper.
2//!
3//! Provides a reusable mechanism to limit log frequency without
4//! performing any logging itself.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9/// A lock-free helper that decides whether logging is allowed at the current moment.
10///
11/// Uses monotonic time (`Instant`) and atomic operations to ensure correct
12/// behavior under concurrency without any locks or allocations on the hot path.
13///
14/// # Example
15///
16/// ```
17/// use std::time::Duration;
18/// use modkit::telemetry::ThrottledLog;
19///
20/// let throttle = ThrottledLog::new(Duration::from_secs(10));
21///
22/// if throttle.should_log() {
23/// // Perform logging here
24/// }
25/// ```
26pub struct ThrottledLog {
27 /// Monotonic start time for computing elapsed milliseconds.
28 start: Instant,
29 /// Next allowed log time in milliseconds since `start`.
30 next_log_ms: AtomicU64,
31 /// Throttle interval in milliseconds.
32 throttle_ms: u64,
33}
34
35fn u64_millis(d: Duration) -> u64 {
36 let ms: u128 = d.as_millis();
37 u64::try_from(ms).unwrap_or(u64::MAX)
38}
39
40impl ThrottledLog {
41 /// Creates a new throttled log helper with the given throttle interval.
42 #[must_use]
43 pub fn new(throttle: Duration) -> Self {
44 Self {
45 start: Instant::now(),
46 next_log_ms: AtomicU64::new(0),
47 throttle_ms: u64_millis(throttle),
48 }
49 }
50
51 /// Returns `true` if logging is allowed at the current moment.
52 ///
53 /// Uses compare-and-swap to ensure that under concurrent calls,
54 /// only one caller per throttle interval receives `true`.
55 pub fn should_log(&self) -> bool {
56 let now_ms = u64_millis(self.start.elapsed());
57 let next = self.next_log_ms.load(Ordering::Relaxed);
58
59 if now_ms < next {
60 return false;
61 }
62
63 let new_next = now_ms.saturating_add(self.throttle_ms);
64 self.next_log_ms
65 .compare_exchange(next, new_next, Ordering::Relaxed, Ordering::Relaxed)
66 .is_ok()
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn first_call_returns_true() {
76 let throttle = ThrottledLog::new(Duration::from_secs(10));
77 assert!(throttle.should_log());
78 }
79
80 #[test]
81 fn second_call_within_interval_returns_false() {
82 let throttle = ThrottledLog::new(Duration::from_secs(10));
83 assert!(throttle.should_log());
84 assert!(!throttle.should_log());
85 }
86}