veilid_tools/
flap_detector.rs1#[derive(Debug, Clone)]
6pub struct FlapDetector<T: PartialEq> {
7 penalty: f64,
8 threshold: f64,
9 half_life_us: f64,
10 last_update_us: u64,
11 flapping: bool,
12 last_value: Option<T>,
13}
14
15impl<T: PartialEq> FlapDetector<T> {
16 #[must_use]
18 pub fn new(threshold: f64, half_life_us: u64) -> Self {
19 Self {
20 penalty: 0.0,
21 threshold,
22 half_life_us: half_life_us as f64,
23 last_update_us: 0,
24 flapping: false,
25 last_value: None,
26 }
27 }
28
29 #[must_use]
31 pub fn new_secs(threshold: f64, half_life_secs: u64) -> Self {
32 Self::new(threshold, half_life_secs * 1_000_000)
33 }
34
35 fn decay(&mut self, now_us: u64) {
36 if self.last_update_us != 0 && self.half_life_us > 0.0 {
37 let elapsed = now_us.saturating_sub(self.last_update_us) as f64;
38 self.penalty *= 0.5_f64.powf(elapsed / self.half_life_us);
39 }
40 self.last_update_us = now_us;
41 }
42
43 pub fn record(&mut self, now_us: u64, value: T) -> Option<f64> {
46 let changed = matches!(&self.last_value, Some(lv) if *lv != value);
47 self.last_value = Some(value);
48 if !changed {
49 return None;
50 }
51 self.record_event(now_us)
52 }
53
54 pub fn record_event(&mut self, now_us: u64) -> Option<f64> {
57 self.decay(now_us);
58 if self.flapping && self.penalty < self.threshold {
59 self.flapping = false;
60 }
61 self.penalty += 1.0;
62 if self.penalty >= self.threshold && !self.flapping {
63 self.flapping = true;
64 return Some(self.penalty);
65 }
66 None
67 }
68
69 pub fn reset(&mut self) {
72 self.penalty = 0.0;
73 self.last_update_us = 0;
74 self.flapping = false;
75 self.last_value = None;
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 const SEC: u64 = 1_000_000;
83
84 #[test]
85 fn stable_value_never_flaps() {
86 let mut fd = FlapDetector::new(3.0, 30 * SEC);
87 for i in 0..10 {
88 assert_eq!(fd.record(i * SEC, true), None);
89 }
90 }
91
92 #[test]
93 fn first_observation_is_not_a_transition() {
94 let mut fd = FlapDetector::new(3.0, 30 * SEC);
95 assert_eq!(fd.record(0, true), None);
96 }
97
98 #[test]
99 fn sustained_flipping_trips_threshold() {
100 let mut fd = FlapDetector::new(3.0, 30 * SEC);
101 let t = 5 * SEC; assert_eq!(fd.record(t, true), None); assert_eq!(fd.record(t, false), None); assert_eq!(fd.record(t, true), None); assert!(fd.record(t, false).is_some()); }
107
108 #[test]
109 fn rearms_after_decay() {
110 let mut fd = FlapDetector::new(3.0, SEC);
111 let t = 10 * SEC;
112 fd.record(t, true);
113 fd.record(t, false);
114 fd.record(t, true);
115 assert!(fd.record(t, false).is_some()); let t2 = t + 1000 * SEC;
118 assert_eq!(fd.record(t2, true), None);
119 assert_eq!(fd.record(t2, false), None);
120 assert!(fd.record(t2, true).is_some());
121 }
122
123 #[test]
124 fn reset_clears_penalty_and_history() {
125 let mut fd = FlapDetector::new(3.0, 30 * SEC);
126 let t = 5 * SEC;
127 fd.record(t, true);
128 fd.record(t, false);
129 fd.record(t, true); fd.reset();
131 assert_eq!(fd.record(t, false), None); assert_eq!(fd.record(t, true), None); assert_eq!(fd.record(t, false), None); assert!(fd.record(t, true).is_some()); }
138}