Skip to main content

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}