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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
This commit is contained in:
rustdesk
2026-08-06 13:41:15 +08:00
parent c58be3034d
commit 9ec46d5cb3
2 changed files with 100 additions and 0 deletions
+1
View File
@@ -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")]
+99
View File
@@ -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<ThrottleState>,
}
struct ThrottleState {
suppressed: u64,
last: Option<Instant>,
}
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<u64> {
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));
}
}