1use std::collections::HashMap;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub enum AnnouncementChannel {
11 Dht,
13 Gossipsub,
15 Both,
17}
18
19#[derive(Clone, Debug)]
21pub struct AnnouncementRecord {
22 pub cid: String,
24 pub channel: AnnouncementChannel,
26 pub announced_at_tick: u64,
28 pub expiry_tick: u64,
30 pub successful: bool,
32 pub retry_count: u32,
34}
35
36impl AnnouncementRecord {
37 pub fn is_expired(&self, current_tick: u64) -> bool {
39 current_tick >= self.expiry_tick
40 }
41
42 pub fn should_retry(&self, max_retries: u32) -> bool {
44 !self.successful && self.retry_count < max_retries
45 }
46}
47
48#[derive(Clone, Debug)]
50pub struct AnnouncementConfig {
51 pub ttl_ticks: u64,
53 pub max_retries: u32,
55 pub reannounce_interval_ticks: u64,
57}
58
59impl Default for AnnouncementConfig {
60 fn default() -> Self {
61 Self {
62 ttl_ticks: 1000,
63 max_retries: 3,
64 reannounce_interval_ticks: 500,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Default)]
71pub struct AnnouncementStats {
72 pub total_announced: usize,
74 pub successful_count: usize,
76 pub failed_count: usize,
78 pub expired_count: usize,
80 pub pending_retry_count: usize,
82}
83
84impl AnnouncementStats {
85 pub fn success_rate(&self) -> f64 {
87 let total = self.successful_count + self.failed_count;
88 if total == 0 {
89 0.0
90 } else {
91 self.successful_count as f64 / total as f64
92 }
93 }
94}
95
96pub struct PeerAnnouncementManager {
98 pub records: HashMap<String, AnnouncementRecord>,
100 pub config: AnnouncementConfig,
102 pub expired_records: Vec<AnnouncementRecord>,
104}
105
106impl PeerAnnouncementManager {
107 pub fn new(config: AnnouncementConfig) -> Self {
109 Self {
110 records: HashMap::new(),
111 config,
112 expired_records: Vec::new(),
113 }
114 }
115
116 pub fn announce(&mut self, cid: &str, channel: AnnouncementChannel, tick: u64) -> bool {
122 if let Some(existing) = self.records.get(cid) {
123 if existing.successful && !existing.is_expired(tick) {
124 return false;
125 }
126 }
127
128 let record = AnnouncementRecord {
129 cid: cid.to_owned(),
130 channel,
131 announced_at_tick: tick,
132 expiry_tick: tick + self.config.ttl_ticks,
133 successful: false,
134 retry_count: 0,
135 };
136 self.records.insert(cid.to_owned(), record);
137 true
138 }
139
140 pub fn mark_success(&mut self, cid: &str) -> bool {
144 match self.records.get_mut(cid) {
145 Some(record) => {
146 record.successful = true;
147 true
148 }
149 None => false,
150 }
151 }
152
153 pub fn mark_failure(&mut self, cid: &str) -> bool {
157 match self.records.get_mut(cid) {
158 Some(record) => {
159 record.retry_count += 1;
160 true
161 }
162 None => false,
163 }
164 }
165
166 pub fn needs_reannounce(&self, cid: &str, current_tick: u64) -> bool {
169 match self.records.get(cid) {
170 Some(record) => {
171 record.successful
172 && !record.is_expired(current_tick)
173 && current_tick.saturating_sub(record.announced_at_tick)
174 >= self.config.reannounce_interval_ticks
175 }
176 None => false,
177 }
178 }
179
180 pub fn evict_expired(&mut self, current_tick: u64) {
182 let expired_keys: Vec<String> = self
183 .records
184 .iter()
185 .filter(|(_, r)| r.is_expired(current_tick))
186 .map(|(k, _)| k.clone())
187 .collect();
188
189 for key in expired_keys {
190 if let Some(record) = self.records.remove(&key) {
191 self.expired_records.push(record);
192 }
193 }
194 }
195
196 pub fn pending_retries(&self, current_tick: u64) -> Vec<&AnnouncementRecord> {
199 let max_retries = self.config.max_retries;
200 let mut pending: Vec<&AnnouncementRecord> = self
201 .records
202 .values()
203 .filter(|r| r.should_retry(max_retries) && !r.is_expired(current_tick))
204 .collect();
205 pending.sort_by_key(|r| r.retry_count);
206 pending
207 }
208
209 pub fn get(&self, cid: &str) -> Option<&AnnouncementRecord> {
211 self.records.get(cid)
212 }
213
214 pub fn stats(&self, current_tick: u64) -> AnnouncementStats {
216 let max_retries = self.config.max_retries;
217 let mut successful_count = 0usize;
218 let mut failed_count = 0usize;
219 let mut pending_retry_count = 0usize;
220
221 for record in self.records.values() {
222 if record.successful {
223 successful_count += 1;
224 } else {
225 failed_count += 1;
226 }
227 if record.should_retry(max_retries) && !record.is_expired(current_tick) {
228 pending_retry_count += 1;
229 }
230 }
231
232 AnnouncementStats {
233 total_announced: self.records.len(),
234 successful_count,
235 failed_count,
236 expired_count: self.expired_records.len(),
237 pending_retry_count,
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn default_manager() -> PeerAnnouncementManager {
247 PeerAnnouncementManager::new(AnnouncementConfig::default())
248 }
249
250 #[test]
253 fn test_new_starts_empty() {
254 let mgr = default_manager();
255 assert!(mgr.records.is_empty());
256 assert!(mgr.expired_records.is_empty());
257 }
258
259 #[test]
262 fn test_announce_creates_record() {
263 let mut mgr = default_manager();
264 let result = mgr.announce("cid1", AnnouncementChannel::Dht, 0);
265 assert!(result);
266 assert!(mgr.get("cid1").is_some());
267 }
268
269 #[test]
270 fn test_announce_sets_fields_correctly() {
271 let mut mgr = default_manager();
272 mgr.announce("cid1", AnnouncementChannel::Gossipsub, 10);
273 let record = mgr
274 .get("cid1")
275 .expect("test: record should exist after announce");
276 assert_eq!(record.channel, AnnouncementChannel::Gossipsub);
277 assert_eq!(record.announced_at_tick, 10);
278 assert_eq!(record.expiry_tick, 10 + mgr.config.ttl_ticks);
279 assert!(!record.successful);
280 assert_eq!(record.retry_count, 0);
281 }
282
283 #[test]
284 fn test_announce_returns_false_for_successful_non_expired() {
285 let mut mgr = default_manager();
286 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
287 mgr.mark_success("cid1");
288 let result = mgr.announce("cid1", AnnouncementChannel::Dht, 1);
290 assert!(!result);
291 }
292
293 #[test]
294 fn test_announce_returns_true_for_failed_record() {
295 let mut mgr = default_manager();
296 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
297 let result = mgr.announce("cid1", AnnouncementChannel::Both, 1);
299 assert!(result);
300 }
301
302 #[test]
303 fn test_announce_returns_true_for_expired_successful_record() {
304 let config = AnnouncementConfig {
305 ttl_ticks: 10,
306 ..Default::default()
307 };
308 let mut mgr = PeerAnnouncementManager::new(config);
309 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
310 mgr.mark_success("cid1");
311 let result = mgr.announce("cid1", AnnouncementChannel::Dht, 10);
313 assert!(result);
314 }
315
316 #[test]
319 fn test_mark_success_sets_flag() {
320 let mut mgr = default_manager();
321 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
322 assert!(mgr.mark_success("cid1"));
323 assert!(
324 mgr.get("cid1")
325 .expect("test: record should exist after mark_success")
326 .successful
327 );
328 }
329
330 #[test]
331 fn test_mark_success_false_for_unknown() {
332 let mut mgr = default_manager();
333 assert!(!mgr.mark_success("unknown"));
334 }
335
336 #[test]
339 fn test_mark_failure_increments_retry_count() {
340 let mut mgr = default_manager();
341 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
342 mgr.mark_failure("cid1");
343 assert_eq!(
344 mgr.get("cid1")
345 .expect("test: record should exist after first mark_failure")
346 .retry_count,
347 1
348 );
349 mgr.mark_failure("cid1");
350 assert_eq!(
351 mgr.get("cid1")
352 .expect("test: record should exist after second mark_failure")
353 .retry_count,
354 2
355 );
356 }
357
358 #[test]
359 fn test_mark_failure_false_for_unknown() {
360 let mut mgr = default_manager();
361 assert!(!mgr.mark_failure("unknown"));
362 }
363
364 #[test]
367 fn test_needs_reannounce_true_when_interval_elapsed() {
368 let config = AnnouncementConfig {
369 reannounce_interval_ticks: 100,
370 ..Default::default()
371 };
372 let mut mgr = PeerAnnouncementManager::new(config);
373 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
374 mgr.mark_success("cid1");
375 assert!(mgr.needs_reannounce("cid1", 100));
376 }
377
378 #[test]
379 fn test_needs_reannounce_false_before_interval() {
380 let config = AnnouncementConfig {
381 reannounce_interval_ticks: 100,
382 ..Default::default()
383 };
384 let mut mgr = PeerAnnouncementManager::new(config);
385 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
386 mgr.mark_success("cid1");
387 assert!(!mgr.needs_reannounce("cid1", 99));
388 }
389
390 #[test]
391 fn test_needs_reannounce_false_for_failed_record() {
392 let mut mgr = default_manager();
393 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
394 assert!(!mgr.needs_reannounce("cid1", 600));
396 }
397
398 #[test]
399 fn test_needs_reannounce_false_for_unknown() {
400 let mgr = default_manager();
401 assert!(!mgr.needs_reannounce("nonexistent", 999));
402 }
403
404 #[test]
407 fn test_is_expired_true_at_expiry() {
408 let record = AnnouncementRecord {
409 cid: "cid1".to_owned(),
410 channel: AnnouncementChannel::Dht,
411 announced_at_tick: 0,
412 expiry_tick: 100,
413 successful: false,
414 retry_count: 0,
415 };
416 assert!(record.is_expired(100));
417 assert!(record.is_expired(101));
418 }
419
420 #[test]
421 fn test_is_expired_false_before_expiry() {
422 let record = AnnouncementRecord {
423 cid: "cid1".to_owned(),
424 channel: AnnouncementChannel::Dht,
425 announced_at_tick: 0,
426 expiry_tick: 100,
427 successful: false,
428 retry_count: 0,
429 };
430 assert!(!record.is_expired(99));
431 }
432
433 #[test]
434 fn test_should_retry_true_when_not_successful_and_retries_remaining() {
435 let record = AnnouncementRecord {
436 cid: "cid1".to_owned(),
437 channel: AnnouncementChannel::Dht,
438 announced_at_tick: 0,
439 expiry_tick: 1000,
440 successful: false,
441 retry_count: 2,
442 };
443 assert!(record.should_retry(3));
444 }
445
446 #[test]
447 fn test_should_retry_false_at_max_retries() {
448 let record = AnnouncementRecord {
449 cid: "cid1".to_owned(),
450 channel: AnnouncementChannel::Dht,
451 announced_at_tick: 0,
452 expiry_tick: 1000,
453 successful: false,
454 retry_count: 3,
455 };
456 assert!(!record.should_retry(3));
457 }
458
459 #[test]
460 fn test_should_retry_false_when_successful() {
461 let record = AnnouncementRecord {
462 cid: "cid1".to_owned(),
463 channel: AnnouncementChannel::Dht,
464 announced_at_tick: 0,
465 expiry_tick: 1000,
466 successful: true,
467 retry_count: 0,
468 };
469 assert!(!record.should_retry(3));
470 }
471
472 #[test]
475 fn test_evict_expired_moves_records() {
476 let config = AnnouncementConfig {
477 ttl_ticks: 10,
478 ..Default::default()
479 };
480 let mut mgr = PeerAnnouncementManager::new(config);
481 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
482 mgr.evict_expired(10); assert!(!mgr.records.contains_key("cid1"));
484 assert_eq!(mgr.expired_records.len(), 1);
485 assert_eq!(mgr.expired_records[0].cid, "cid1");
486 }
487
488 #[test]
489 fn test_evict_expired_keeps_fresh_records() {
490 let config = AnnouncementConfig {
491 ttl_ticks: 100,
492 ..Default::default()
493 };
494 let mut mgr = PeerAnnouncementManager::new(config);
495 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
496 mgr.evict_expired(50); assert!(mgr.records.contains_key("cid1"));
498 assert!(mgr.expired_records.is_empty());
499 }
500
501 #[test]
502 fn test_evict_expired_mixed() {
503 let config = AnnouncementConfig {
504 ttl_ticks: 10,
505 ..Default::default()
506 };
507 let mut mgr = PeerAnnouncementManager::new(config);
508 mgr.announce("old", AnnouncementChannel::Dht, 0); mgr.announce("new", AnnouncementChannel::Dht, 5); mgr.evict_expired(12);
511 assert!(!mgr.records.contains_key("old"));
512 assert!(mgr.records.contains_key("new"));
513 assert_eq!(mgr.expired_records.len(), 1);
514 }
515
516 #[test]
519 fn test_pending_retries_filtered_correctly() {
520 let config = AnnouncementConfig {
521 max_retries: 3,
522 ttl_ticks: 1000,
523 ..Default::default()
524 };
525 let mut mgr = PeerAnnouncementManager::new(config);
526 mgr.announce("a", AnnouncementChannel::Dht, 0);
527 mgr.mark_failure("a"); mgr.announce("b", AnnouncementChannel::Dht, 0);
529 mgr.mark_success("b"); let pending = mgr.pending_retries(0);
532 assert_eq!(pending.len(), 1);
533 assert_eq!(pending[0].cid, "a");
534 }
535
536 #[test]
537 fn test_pending_retries_sorted_by_retry_count_asc() {
538 let config = AnnouncementConfig {
539 max_retries: 5,
540 ttl_ticks: 1000,
541 ..Default::default()
542 };
543 let mut mgr = PeerAnnouncementManager::new(config);
544 mgr.announce("a", AnnouncementChannel::Dht, 0);
545 mgr.mark_failure("a");
546 mgr.mark_failure("a"); mgr.announce("b", AnnouncementChannel::Dht, 0);
549 mgr.mark_failure("b"); mgr.announce("c", AnnouncementChannel::Dht, 0);
552 mgr.mark_failure("c");
553 mgr.mark_failure("c");
554 mgr.mark_failure("c"); let pending = mgr.pending_retries(0);
557 assert_eq!(pending.len(), 3);
558 assert_eq!(pending[0].retry_count, 1);
560 assert_eq!(pending[1].retry_count, 2);
561 assert_eq!(pending[2].retry_count, 3);
562 }
563
564 #[test]
565 fn test_pending_retries_excludes_expired() {
566 let config = AnnouncementConfig {
567 max_retries: 3,
568 ttl_ticks: 10,
569 ..Default::default()
570 };
571 let mut mgr = PeerAnnouncementManager::new(config);
572 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
573 mgr.mark_failure("cid1");
574 let pending = mgr.pending_retries(10);
576 assert!(pending.is_empty());
577 }
578
579 #[test]
582 fn test_stats_success_rate_computed() {
583 let mut mgr = default_manager();
584 mgr.announce("a", AnnouncementChannel::Dht, 0);
585 mgr.mark_success("a");
586 mgr.announce("b", AnnouncementChannel::Dht, 0);
587 let stats = mgr.stats(0);
590 assert_eq!(stats.successful_count, 1);
591 assert_eq!(stats.failed_count, 1);
592 assert!((stats.success_rate() - 0.5).abs() < f64::EPSILON);
593 }
594
595 #[test]
596 fn test_stats_success_rate_zero_when_both_zero() {
597 let mgr = default_manager();
598 let stats = mgr.stats(0);
599 assert_eq!(stats.success_rate(), 0.0);
600 }
601
602 #[test]
603 fn test_stats_expired_count_reflects_evictions() {
604 let config = AnnouncementConfig {
605 ttl_ticks: 5,
606 ..Default::default()
607 };
608 let mut mgr = PeerAnnouncementManager::new(config);
609 mgr.announce("cid1", AnnouncementChannel::Dht, 0);
610 mgr.evict_expired(5);
611
612 let stats = mgr.stats(5);
613 assert_eq!(stats.expired_count, 1);
614 assert_eq!(stats.total_announced, 0);
615 }
616
617 #[test]
618 fn test_stats_pending_retry_count() {
619 let config = AnnouncementConfig {
620 max_retries: 3,
621 ttl_ticks: 1000,
622 ..Default::default()
623 };
624 let mut mgr = PeerAnnouncementManager::new(config);
625 mgr.announce("a", AnnouncementChannel::Dht, 0);
626 mgr.mark_failure("a");
627
628 let stats = mgr.stats(0);
629 assert_eq!(stats.pending_retry_count, 1);
630 }
631}