1use parking_lot::RwLock;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
15pub struct MeshRepairConfig {
16 pub d_low: usize,
18 pub d_target: usize,
20 pub d_high: usize,
22 pub repair_interval_ms: u64,
25}
26
27impl Default for MeshRepairConfig {
28 fn default() -> Self {
29 Self {
30 d_low: 6,
31 d_target: 8,
32 d_high: 12,
33 repair_interval_ms: 5_000,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
42pub struct MeshRepairState {
43 pub topic: String,
45 pub current_peers: usize,
47 pub last_repair_ms: u64,
50 pub repair_count: u64,
52 pub prune_count: u64,
54}
55
56impl MeshRepairState {
57 fn new(topic: impl Into<String>) -> Self {
58 Self {
59 topic: topic.into(),
60 current_peers: 0,
61 last_repair_ms: 0,
62 repair_count: 0,
63 prune_count: 0,
64 }
65 }
66}
67
68pub struct MeshRepairCoordinator {
76 config: MeshRepairConfig,
77 topic_states: RwLock<HashMap<String, MeshRepairState>>,
79}
80
81impl MeshRepairCoordinator {
82 pub fn new(config: MeshRepairConfig) -> Self {
84 Self {
85 config,
86 topic_states: RwLock::new(HashMap::new()),
87 }
88 }
89
90 pub fn record_peer_count(&self, topic: &str, count: usize, now_ms: u64) {
96 let mut map = self.topic_states.write();
97 let state = map
98 .entry(topic.to_string())
99 .or_insert_with(|| MeshRepairState::new(topic));
100 state.current_peers = count;
101 let _ = now_ms; }
107
108 pub fn record_repair(&self, topic: &str, now_ms: u64) {
110 let mut map = self.topic_states.write();
111 let state = map
112 .entry(topic.to_string())
113 .or_insert_with(|| MeshRepairState::new(topic));
114 state.last_repair_ms = now_ms;
115 state.repair_count = state.repair_count.saturating_add(1);
116 }
117
118 pub fn record_prune(&self, topic: &str) {
120 let mut map = self.topic_states.write();
121 let state = map
122 .entry(topic.to_string())
123 .or_insert_with(|| MeshRepairState::new(topic));
124 state.prune_count = state.prune_count.saturating_add(1);
125 }
126
127 pub fn needs_repair(&self, topic: &str, now_ms: u64) -> bool {
136 let map = self.topic_states.read();
137 match map.get(topic) {
138 None => false,
139 Some(state) => {
140 let below_threshold = state.current_peers < self.config.d_low;
141 let cooldown_elapsed = state.last_repair_ms == 0
142 || now_ms.saturating_sub(state.last_repair_ms)
143 >= self.config.repair_interval_ms;
144 below_threshold && cooldown_elapsed
145 }
146 }
147 }
148
149 pub fn needs_prune(&self, topic: &str) -> bool {
152 let map = self.topic_states.read();
153 match map.get(topic) {
154 None => false,
155 Some(state) => state.current_peers > self.config.d_high,
156 }
157 }
158
159 pub fn repair_state(&self, topic: &str) -> Option<MeshRepairState> {
162 self.topic_states.read().get(topic).cloned()
163 }
164
165 pub fn topics_needing_repair(&self, now_ms: u64) -> Vec<String> {
167 let map = self.topic_states.read();
168 map.values()
169 .filter(|s| {
170 let below = s.current_peers < self.config.d_low;
171 let cooldown = s.last_repair_ms == 0
172 || now_ms.saturating_sub(s.last_repair_ms) >= self.config.repair_interval_ms;
173 below && cooldown
174 })
175 .map(|s| s.topic.clone())
176 .collect()
177 }
178
179 pub fn topics_needing_prune(&self) -> Vec<String> {
181 let map = self.topic_states.read();
182 map.values()
183 .filter(|s| s.current_peers > self.config.d_high)
184 .map(|s| s.topic.clone())
185 .collect()
186 }
187
188 pub fn all_states(&self) -> Vec<MeshRepairState> {
190 self.topic_states.read().values().cloned().collect()
191 }
192
193 pub fn config(&self) -> &MeshRepairConfig {
195 &self.config
196 }
197}
198
199#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn default_coordinator() -> MeshRepairCoordinator {
206 MeshRepairCoordinator::new(MeshRepairConfig::default())
207 }
208
209 const T: &str = "/ipfrs/test/1.0.0";
210
211 #[test]
214 fn test_initial_no_repair_needed() {
215 let coord = default_coordinator();
216 assert!(!coord.needs_repair(T, 10_000));
218 }
219
220 #[test]
221 fn test_needs_repair_below_d_low() {
222 let coord = default_coordinator();
223 coord.record_peer_count(T, 5, 0);
226 assert!(coord.needs_repair(T, 10_000));
227 }
228
229 #[test]
230 fn test_no_repair_if_too_recent() {
231 let coord = default_coordinator();
232 coord.record_peer_count(T, 3, 0);
234 coord.record_repair(T, 1_000);
236 assert!(!coord.needs_repair(T, 5_999));
238 assert!(coord.needs_repair(T, 6_000));
240 }
241
242 #[test]
243 fn test_needs_prune_above_d_high() {
244 let coord = default_coordinator();
245 coord.record_peer_count(T, 13, 0);
247 assert!(coord.needs_prune(T));
248 }
249
250 #[test]
251 fn test_record_repair_updates_state() {
252 let coord = default_coordinator();
253 coord.record_peer_count(T, 2, 0);
254 coord.record_repair(T, 3_000);
255 coord.record_repair(T, 8_000);
256
257 let state = coord.repair_state(T).expect("state present");
258 assert_eq!(state.last_repair_ms, 8_000);
259 assert_eq!(state.repair_count, 2);
260 }
261
262 #[test]
263 fn test_topics_needing_repair_multiple_topics() {
264 let coord = default_coordinator();
265 let t1 = "/ipfrs/t1/1.0.0";
266 let t2 = "/ipfrs/t2/1.0.0";
267 let t3 = "/ipfrs/t3/1.0.0";
268
269 coord.record_peer_count(t1, 3, 0);
271 coord.record_peer_count(t2, 8, 0);
273 coord.record_peer_count(t3, 2, 0);
275 coord.record_repair(t3, 99_000);
276
277 let needing = coord.topics_needing_repair(100_000); assert_eq!(needing.len(), 1);
279 assert_eq!(needing[0], t1);
280 }
281
282 #[test]
283 fn test_topics_needing_prune() {
284 let coord = default_coordinator();
285 let t1 = "/ipfrs/t1/1.0.0";
286 let t2 = "/ipfrs/t2/1.0.0";
287
288 coord.record_peer_count(t1, 15, 0); coord.record_peer_count(t2, 10, 0); let needing = coord.topics_needing_prune();
292 assert_eq!(needing.len(), 1);
293 assert_eq!(needing[0], t1);
294 }
295
296 #[test]
299 fn test_no_prune_when_at_threshold() {
300 let coord = default_coordinator();
301 coord.record_peer_count(T, 12, 0); assert!(!coord.needs_prune(T));
303 }
304
305 #[test]
306 fn test_no_repair_at_d_low_exactly() {
307 let coord = default_coordinator();
308 coord.record_peer_count(T, 6, 0); assert!(!coord.needs_repair(T, 10_000));
310 }
311
312 #[test]
313 fn test_record_prune_increments_count() {
314 let coord = default_coordinator();
315 coord.record_peer_count(T, 14, 0);
316 coord.record_prune(T);
317 coord.record_prune(T);
318 let state = coord.repair_state(T).expect("state present");
319 assert_eq!(state.prune_count, 2);
320 }
321
322 #[test]
323 fn test_all_states_returns_all_topics() {
324 let coord = default_coordinator();
325 coord.record_peer_count("/t1", 5, 0);
326 coord.record_peer_count("/t2", 8, 0);
327 coord.record_peer_count("/t3", 14, 0);
328 assert_eq!(coord.all_states().len(), 3);
329 }
330}