clawft_kernel/
mesh_dedup.rs1use std::collections::HashMap;
22use std::sync::Arc;
23use std::time::Duration;
24
25use crate::mesh_clock::{Clock, MonoTime, RealClock};
26
27pub struct DedupFilter {
33 seen: HashMap<String, MonoTime>,
35 ttl: Duration,
37 max_entries: usize,
39 clock: Arc<dyn Clock>,
41}
42
43impl DedupFilter {
44 pub fn new(ttl: Duration, max_entries: usize) -> Self {
48 Self::with_clock(ttl, max_entries, RealClock::shared())
49 }
50
51 pub fn with_clock(ttl: Duration, max_entries: usize, clock: Arc<dyn Clock>) -> Self {
53 Self {
54 seen: HashMap::new(),
55 ttl,
56 max_entries,
57 clock,
58 }
59 }
60
61 pub fn default_mesh() -> Self {
63 Self::new(Duration::from_secs(60), 10_000)
64 }
65
66 pub fn check_and_insert(&mut self, id: &str) -> bool {
69 self.evict_expired();
70
71 if self.seen.contains_key(id) {
72 return false; }
74
75 if self.seen.len() >= self.max_entries {
77 self.evict_oldest();
78 }
79
80 self.seen.insert(id.to_string(), self.clock.now());
81 true }
83
84 pub fn is_duplicate(&self, id: &str) -> bool {
86 if let Some(inserted) = self.seen.get(id) {
87 self.clock.now().elapsed_since(*inserted) < self.ttl
88 } else {
89 false
90 }
91 }
92
93 pub fn len(&self) -> usize {
95 self.seen.len()
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.seen.is_empty()
101 }
102
103 fn evict_expired(&mut self) {
105 let now = self.clock.now();
106 let ttl = self.ttl;
107 self.seen
108 .retain(|_, inserted| now.elapsed_since(*inserted) < ttl);
109 }
110
111 fn evict_oldest(&mut self) {
113 if let Some(oldest_key) = self
114 .seen
115 .iter()
116 .min_by_key(|(_, t)| **t)
117 .map(|(k, _)| k.clone())
118 {
119 self.seen.remove(&oldest_key);
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::mesh_clock::MockClock;
128
129 #[test]
130 fn new_message_returns_true() {
131 let mut f = DedupFilter::new(Duration::from_secs(10), 100);
132 assert!(f.check_and_insert("msg-1"));
133 }
134
135 #[test]
136 fn duplicate_returns_false() {
137 let mut f = DedupFilter::new(Duration::from_secs(10), 100);
138 assert!(f.check_and_insert("msg-1"));
139 assert!(!f.check_and_insert("msg-1"));
140 }
141
142 #[test]
143 fn different_ids_both_new() {
144 let mut f = DedupFilter::new(Duration::from_secs(10), 100);
145 assert!(f.check_and_insert("a"));
146 assert!(f.check_and_insert("b"));
147 assert_eq!(f.len(), 2);
148 }
149
150 #[test]
151 fn is_duplicate_without_inserting() {
152 let mut f = DedupFilter::new(Duration::from_secs(10), 100);
153 assert!(!f.is_duplicate("x"));
154 f.check_and_insert("x");
155 assert!(f.is_duplicate("x"));
156 }
157
158 #[test]
159 fn expired_entries_evicted() {
160 let clock = MockClock::new();
161 let mut f = DedupFilter::with_clock(
162 Duration::from_secs(1),
163 100,
164 Arc::new(clock.clone()),
165 );
166 assert!(f.check_and_insert("old"));
167 clock.advance(Duration::from_secs(2));
168 assert!(f.check_and_insert("old"));
170 }
171
172 #[test]
173 fn max_capacity_evicts_oldest() {
174 let clock = MockClock::new();
175 let mut f =
176 DedupFilter::with_clock(Duration::from_secs(60), 2, Arc::new(clock.clone()));
177 f.check_and_insert("a");
178 clock.advance(Duration::from_secs(1));
179 f.check_and_insert("b");
180 clock.advance(Duration::from_secs(1));
181 f.check_and_insert("c");
183 assert_eq!(f.len(), 2);
184 assert!(!f.is_duplicate("a"));
185 assert!(f.is_duplicate("b"));
186 assert!(f.is_duplicate("c"));
187 }
188
189 #[test]
190 fn empty_filter() {
191 let f = DedupFilter::default_mesh();
192 assert!(f.is_empty());
193 assert_eq!(f.len(), 0);
194 }
195
196 #[test]
197 fn default_mesh_settings() {
198 let f = DedupFilter::default_mesh();
199 assert_eq!(f.max_entries, 10_000);
200 assert_eq!(f.ttl, Duration::from_secs(60));
201 }
202}