From d18dcee6a1f6ee85f897747bb65967bec718a44c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 6 Aug 2026 13:41:15 +0800 Subject: [PATCH] feat: add LogThrottle for sites whose rate a peer controls Debug output is written to the log file, so a log site that fires per received message lets whoever is sending decide how much a machine writes to disk. Dropping the line instead would hide real faults, so collapse it: one line per interval carrying the count of everything suppressed since the last one, with the first occurrence after a quiet period always reported so an isolated fault is not delayed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ --- src/lib.rs | 1 + src/log_throttle.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/log_throttle.rs diff --git a/src/lib.rs b/src/lib.rs index 2b3564219..c33244dd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ pub use toml; pub use uuid; pub mod fingerprint; pub use flexi_logger; +pub mod log_throttle; pub mod stream; pub mod websocket; #[cfg(feature = "webrtc")] diff --git a/src/log_throttle.rs b/src/log_throttle.rs new file mode 100644 index 000000000..109ba8538 --- /dev/null +++ b/src/log_throttle.rs @@ -0,0 +1,99 @@ +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +/// Collapses a log site whose call rate is set by someone else — a peer's message rate, or a +/// retry loop — into at most one line per interval. +/// +/// Debug output is written to the log file, so a site that fires per received packet lets a +/// peer decide how much a machine writes to disk. Dropping the line entirely instead would +/// hide real faults, so keep one line per interval and carry the count of everything +/// suppressed since the last one. +/// +/// Declare one per site (they do not share counts): +/// +/// ```ignore +/// static DROPPED_ICE: LogThrottle = LogThrottle::new(Duration::from_secs(60)); +/// +/// if let Some(n) = DROPPED_ICE.due() { +/// log::debug!("dropped {n} ICE candidate(s) with no route"); +/// } +/// ``` +pub struct LogThrottle { + interval: Duration, + state: Mutex, +} + +struct ThrottleState { + suppressed: u64, + last: Option, +} + +impl LogThrottle { + pub const fn new(interval: Duration) -> Self { + Self { + interval, + state: Mutex::new(ThrottleState { + suppressed: 0, + last: None, + }), + } + } + + /// Record one occurrence. Returns the number of occurrences to report (including this one) + /// when a line is due, or `None` while still inside the interval. + /// + /// The first occurrence after a quiet period always reports, so an isolated fault is not + /// delayed by the interval. + pub fn due(&self) -> Option { + let Ok(mut state) = self.state.lock() else { + // A poisoned mutex means another thread panicked mid-update; the count is not worth + // propagating that, and staying silent is better than logging per call. + return None; + }; + state.suppressed += 1; + let due = state + .last + .map_or(true, |last| last.elapsed() >= self.interval); + if !due { + return None; + } + state.last = Some(Instant::now()); + Some(std::mem::replace(&mut state.suppressed, 0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn first_call_reports_immediately() { + let t = LogThrottle::new(Duration::from_secs(60)); + assert_eq!(t.due(), Some(1)); + } + + #[test] + fn calls_inside_the_interval_are_counted_not_reported() { + let t = LogThrottle::new(Duration::from_secs(60)); + assert_eq!(t.due(), Some(1)); + for _ in 0..100 { + assert_eq!(t.due(), None); + } + } + + #[test] + fn the_next_due_line_carries_everything_suppressed() { + let t = LogThrottle::new(Duration::ZERO); + assert_eq!(t.due(), Some(1)); + // A zero interval is always due, so each call reports exactly itself. + assert_eq!(t.due(), Some(1)); + + let t = LogThrottle::new(Duration::from_millis(30)); + assert_eq!(t.due(), Some(1)); + assert_eq!(t.due(), None); + assert_eq!(t.due(), None); + std::thread::sleep(Duration::from_millis(40)); + // The two suppressed calls plus this one. + assert_eq!(t.due(), Some(3)); + } +}