ddns_a/monitor/
debounce.rs

1//! Debounce policy for event stream processing.
2
3use std::time::Duration;
4
5/// Policy for debouncing IP change events.
6///
7/// Debouncing merges changes that occur within a time window, avoiding
8/// rapid consecutive triggers (flapping) from causing duplicate notifications.
9///
10/// # Merge Semantics
11///
12/// | Scenario | Event Sequence in Window | Output | Reason |
13/// |----------|--------------------------|--------|--------|
14/// | Flicker | `Added(IP) → Removed(IP)` | Empty | Same IP add/remove cancel out |
15/// | Reverse Flicker | `Removed(IP) → Added(IP)` | Empty | Same IP remove/add cancel out |
16/// | Replacement | `Removed(old) → Added(new)` | Both events | Different IPs, independent |
17/// | Duplicate Add | `Added(IP) → Added(IP)` | One Added | Idempotent merge |
18///
19/// # Implementation
20///
21/// At window end, compute net change for each (adapter, address):
22/// - Net > 0: Output `Added`
23/// - Net < 0: Output `Removed`
24/// - Net = 0: No output (cancelled out)
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct DebouncePolicy {
27    /// The debounce window duration.
28    ///
29    /// Changes occurring within this window are merged before emission.
30    window: Duration,
31}
32
33impl DebouncePolicy {
34    /// Creates a new debounce policy with the specified window duration.
35    #[must_use]
36    pub const fn new(window: Duration) -> Self {
37        Self { window }
38    }
39
40    /// Returns the debounce window duration.
41    #[must_use]
42    pub const fn window(&self) -> Duration {
43        self.window
44    }
45}
46
47impl Default for DebouncePolicy {
48    /// Creates a default debounce policy with a 2-second window.
49    ///
50    /// The 2-second default balances responsiveness with protection
51    /// against rapid changes during network configuration updates.
52    fn default() -> Self {
53        Self {
54            window: Duration::from_secs(2),
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn default_window_is_two_seconds() {
65        let policy = DebouncePolicy::default();
66        assert_eq!(policy.window(), Duration::from_secs(2));
67    }
68
69    #[test]
70    fn new_creates_with_specified_window() {
71        let policy = DebouncePolicy::new(Duration::from_millis(500));
72        assert_eq!(policy.window(), Duration::from_millis(500));
73    }
74
75    #[test]
76    fn window_accessor_returns_duration() {
77        let policy = DebouncePolicy::new(Duration::from_secs(5));
78        assert_eq!(policy.window(), Duration::from_secs(5));
79    }
80
81    #[test]
82    fn equality_based_on_window() {
83        let policy1 = DebouncePolicy::new(Duration::from_secs(1));
84        let policy2 = DebouncePolicy::new(Duration::from_secs(1));
85        let policy3 = DebouncePolicy::new(Duration::from_secs(2));
86
87        assert_eq!(policy1, policy2);
88        assert_ne!(policy1, policy3);
89    }
90
91    #[test]
92    fn clone_creates_identical_policy() {
93        let original = DebouncePolicy::new(Duration::from_millis(100));
94        let cloned = original.clone();
95
96        assert_eq!(original, cloned);
97    }
98
99    #[test]
100    fn debug_format_includes_window() {
101        let policy = DebouncePolicy::new(Duration::from_secs(3));
102        let debug_str = format!("{policy:?}");
103
104        assert!(debug_str.contains("DebouncePolicy"));
105        assert!(debug_str.contains("window"));
106    }
107}