1use std::collections::VecDeque;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12
13use parking_lot::RwLock;
14
15#[derive(Debug, Clone)]
21pub struct PeerChurnEvent {
22 pub peer_id: String,
24 pub event_type: ChurnEventType,
26 pub timestamp_ms: u64,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum ChurnEventType {
33 Joined,
35 Left,
37 TimedOut,
39}
40
41#[derive(Debug, Clone)]
43pub struct ChurnMetrics {
44 pub window_ms: u64,
46 pub joins: usize,
48 pub leaves: usize,
50 pub churn_rate: f64,
52 pub stability_score: f64,
54}
55
56#[derive(Debug, Clone)]
62pub struct AdaptiveRefreshConfig {
63 pub base_interval_ms: u64,
65 pub min_interval_ms: u64,
67 pub max_interval_ms: u64,
69 pub high_churn_threshold: f64,
72 pub window_ms: u64,
75 pub max_events: usize,
77}
78
79impl Default for AdaptiveRefreshConfig {
80 fn default() -> Self {
81 Self {
82 base_interval_ms: 10 * 60 * 1_000, min_interval_ms: 60 * 1_000, max_interval_ms: 30 * 60 * 1_000, high_churn_threshold: 2.0,
86 window_ms: 60 * 1_000, max_events: 10_000,
88 }
89 }
90}
91
92pub struct PeerChurnTracker {
98 events: RwLock<VecDeque<PeerChurnEvent>>,
99 config: AdaptiveRefreshConfig,
100 total_joins: AtomicU64,
101 total_leaves: AtomicU64,
102}
103
104impl PeerChurnTracker {
105 pub fn new(config: AdaptiveRefreshConfig) -> Self {
107 Self {
108 events: RwLock::new(VecDeque::new()),
109 config,
110 total_joins: AtomicU64::new(0),
111 total_leaves: AtomicU64::new(0),
112 }
113 }
114
115 pub fn record_event(&self, event: PeerChurnEvent) {
117 match event.event_type {
118 ChurnEventType::Joined => {
119 self.total_joins.fetch_add(1, Ordering::Relaxed);
120 }
121 ChurnEventType::Left | ChurnEventType::TimedOut => {
122 self.total_leaves.fetch_add(1, Ordering::Relaxed);
123 }
124 }
125
126 let mut guard = self.events.write();
127 guard.push_back(event);
128
129 while guard.len() > self.config.max_events {
131 guard.pop_front();
132 }
133 }
134
135 pub fn evict_old_events(&self, now_ms: u64) -> usize {
139 let cutoff = now_ms.saturating_sub(self.config.window_ms);
140 let mut guard = self.events.write();
141 let before = guard.len();
142 while let Some(front) = guard.front() {
143 if front.timestamp_ms < cutoff {
144 guard.pop_front();
145 } else {
146 break;
147 }
148 }
149 before - guard.len()
150 }
151
152 pub fn metrics(&self, now_ms: u64) -> ChurnMetrics {
154 let cutoff = now_ms.saturating_sub(self.config.window_ms);
155 let guard = self.events.read();
156
157 let mut joins = 0usize;
158 let mut leaves = 0usize;
159
160 for ev in guard.iter() {
161 if ev.timestamp_ms >= cutoff {
162 match ev.event_type {
163 ChurnEventType::Joined => joins += 1,
164 ChurnEventType::Left | ChurnEventType::TimedOut => leaves += 1,
165 }
166 }
167 }
168
169 let window_secs = self.config.window_ms as f64 / 1_000.0;
170 let total_events = (joins + leaves) as f64;
171 let churn_rate = if window_secs > 0.0 {
172 total_events / window_secs
173 } else {
174 0.0
175 };
176 let stability_score = 1.0 / (1.0 + churn_rate);
177
178 ChurnMetrics {
179 window_ms: self.config.window_ms,
180 joins,
181 leaves,
182 churn_rate,
183 stability_score,
184 }
185 }
186
187 pub fn recommended_interval(&self, now_ms: u64) -> Duration {
199 let m = self.metrics(now_ms);
200 let rate = m.churn_rate;
201 let cfg = &self.config;
202
203 let interval_ms = if rate >= cfg.high_churn_threshold {
204 let t = ((rate - cfg.high_churn_threshold) / cfg.high_churn_threshold).clamp(0.0, 1.0);
206 let base = cfg.base_interval_ms as f64;
207 let min = cfg.min_interval_ms as f64;
208 let v = base + t * (min - base);
209 (v as u64).max(cfg.min_interval_ms)
210 } else if rate < 0.1 {
211 let t = (1.0 - rate / 0.1).clamp(0.0, 1.0);
213 let base = cfg.base_interval_ms as f64;
214 let max = cfg.max_interval_ms as f64;
215 let v = base + t * (max - base);
216 (v as u64).min(cfg.max_interval_ms)
217 } else {
218 cfg.base_interval_ms
219 };
220
221 Duration::from_millis(interval_ms)
222 }
223
224 pub fn window_event_count(&self, now_ms: u64) -> usize {
226 let cutoff = now_ms.saturating_sub(self.config.window_ms);
227 let guard = self.events.read();
228 guard.iter().filter(|e| e.timestamp_ms >= cutoff).count()
229 }
230
231 pub fn total_joins(&self) -> u64 {
233 self.total_joins.load(Ordering::Relaxed)
234 }
235
236 pub fn total_leaves(&self) -> u64 {
238 self.total_leaves.load(Ordering::Relaxed)
239 }
240}
241
242pub struct AdaptiveRefreshScheduler {
248 tracker: Arc<PeerChurnTracker>,
249 last_refresh_ms: AtomicU64,
250 refresh_count: AtomicU64,
251}
252
253impl AdaptiveRefreshScheduler {
254 const NEVER_REFRESHED: u64 = u64::MAX;
256
257 pub fn new(config: AdaptiveRefreshConfig) -> Arc<Self> {
259 Arc::new(Self {
260 tracker: Arc::new(PeerChurnTracker::new(config)),
261 last_refresh_ms: AtomicU64::new(Self::NEVER_REFRESHED),
262 refresh_count: AtomicU64::new(0),
263 })
264 }
265
266 pub fn record_peer_event(&self, event: PeerChurnEvent) {
268 self.tracker.record_event(event);
269 }
270
271 pub fn is_refresh_due(&self, now_ms: u64) -> bool {
274 let last = self.last_refresh_ms.load(Ordering::Relaxed);
275 if last == Self::NEVER_REFRESHED {
276 return true;
278 }
279 let elapsed_ms = now_ms.saturating_sub(last);
280 let interval_ms = self.tracker.recommended_interval(now_ms).as_millis() as u64;
281 elapsed_ms >= interval_ms
282 }
283
284 pub fn mark_refreshed(&self, now_ms: u64) {
286 self.last_refresh_ms.store(now_ms, Ordering::Relaxed);
287 self.refresh_count.fetch_add(1, Ordering::Relaxed);
288 }
289
290 pub fn current_interval(&self, now_ms: u64) -> Duration {
292 self.tracker.recommended_interval(now_ms)
293 }
294
295 pub fn time_until_refresh(&self, now_ms: u64) -> Duration {
299 let last = self.last_refresh_ms.load(Ordering::Relaxed);
300 if last == Self::NEVER_REFRESHED {
301 return Duration::ZERO;
302 }
303 let interval_ms = self.tracker.recommended_interval(now_ms).as_millis() as u64;
304 let elapsed_ms = now_ms.saturating_sub(last);
305 if elapsed_ms >= interval_ms {
306 Duration::ZERO
307 } else {
308 Duration::from_millis(interval_ms - elapsed_ms)
309 }
310 }
311
312 pub fn refresh_count(&self) -> u64 {
314 self.refresh_count.load(Ordering::Relaxed)
315 }
316
317 pub fn churn_metrics(&self, now_ms: u64) -> ChurnMetrics {
319 self.tracker.metrics(now_ms)
320 }
321}
322
323pub struct ChurnResilienceManager {
329 pub scheduler: Arc<AdaptiveRefreshScheduler>,
331}
332
333impl ChurnResilienceManager {
334 pub fn new(config: AdaptiveRefreshConfig) -> Arc<Self> {
336 Arc::new(Self {
337 scheduler: AdaptiveRefreshScheduler::new(config),
338 })
339 }
340
341 pub fn peer_joined(&self, peer_id: impl Into<String>, now_ms: u64) {
343 self.scheduler.record_peer_event(PeerChurnEvent {
344 peer_id: peer_id.into(),
345 event_type: ChurnEventType::Joined,
346 timestamp_ms: now_ms,
347 });
348 }
349
350 pub fn peer_left(&self, peer_id: impl Into<String>, now_ms: u64) {
352 self.scheduler.record_peer_event(PeerChurnEvent {
353 peer_id: peer_id.into(),
354 event_type: ChurnEventType::Left,
355 timestamp_ms: now_ms,
356 });
357 }
358
359 pub fn peer_timed_out(&self, peer_id: impl Into<String>, now_ms: u64) {
361 self.scheduler.record_peer_event(PeerChurnEvent {
362 peer_id: peer_id.into(),
363 event_type: ChurnEventType::TimedOut,
364 timestamp_ms: now_ms,
365 });
366 }
367
368 pub fn should_refresh(&self, now_ms: u64) -> bool {
370 self.scheduler.is_refresh_due(now_ms)
371 }
372
373 pub fn mark_refreshed(&self, now_ms: u64) {
375 self.scheduler.mark_refreshed(now_ms);
376 }
377
378 pub fn metrics(&self, now_ms: u64) -> ChurnMetrics {
380 self.scheduler.churn_metrics(now_ms)
381 }
382}
383
384#[cfg(test)]
389mod tests {
390 use super::*;
391
392 fn default_config() -> AdaptiveRefreshConfig {
393 AdaptiveRefreshConfig::default()
394 }
395
396 fn ts(secs: u64) -> u64 {
398 secs * 1_000
399 }
400
401 #[test]
406 fn test_record_event_increments_totals() {
407 let tracker = PeerChurnTracker::new(default_config());
408 tracker.record_event(PeerChurnEvent {
409 peer_id: "p1".into(),
410 event_type: ChurnEventType::Joined,
411 timestamp_ms: ts(10),
412 });
413 tracker.record_event(PeerChurnEvent {
414 peer_id: "p2".into(),
415 event_type: ChurnEventType::Left,
416 timestamp_ms: ts(11),
417 });
418 tracker.record_event(PeerChurnEvent {
419 peer_id: "p3".into(),
420 event_type: ChurnEventType::TimedOut,
421 timestamp_ms: ts(12),
422 });
423 assert_eq!(tracker.total_joins(), 1);
424 assert_eq!(tracker.total_leaves(), 2);
425 }
426
427 #[test]
428 fn test_evict_old_events() {
429 let cfg = AdaptiveRefreshConfig {
430 window_ms: 60_000,
431 ..default_config()
432 };
433 let tracker = PeerChurnTracker::new(cfg);
434 tracker.record_event(PeerChurnEvent {
436 peer_id: "a".into(),
437 event_type: ChurnEventType::Joined,
438 timestamp_ms: 0,
439 });
440 tracker.record_event(PeerChurnEvent {
441 peer_id: "b".into(),
442 event_type: ChurnEventType::Left,
443 timestamp_ms: 0,
444 });
445 tracker.record_event(PeerChurnEvent {
446 peer_id: "c".into(),
447 event_type: ChurnEventType::Joined,
448 timestamp_ms: ts(120),
449 });
450
451 let evicted = tracker.evict_old_events(ts(180));
453 assert_eq!(evicted, 2);
454 assert_eq!(tracker.window_event_count(ts(180)), 1);
455 }
456
457 #[test]
458 fn test_metrics_empty_window() {
459 let tracker = PeerChurnTracker::new(default_config());
460 let m = tracker.metrics(ts(100));
461 assert_eq!(m.joins, 0);
462 assert_eq!(m.leaves, 0);
463 assert!((m.churn_rate - 0.0).abs() < f64::EPSILON);
464 assert!((m.stability_score - 1.0).abs() < 1e-9);
466 }
467
468 #[test]
469 fn test_metrics_with_events() {
470 let cfg = AdaptiveRefreshConfig {
471 window_ms: 60_000,
472 ..default_config()
473 };
474 let tracker = PeerChurnTracker::new(cfg);
475
476 for i in 0..10u64 {
478 tracker.record_event(PeerChurnEvent {
479 peer_id: format!("join-{i}"),
480 event_type: ChurnEventType::Joined,
481 timestamp_ms: ts(50) + i,
482 });
483 }
484 for i in 0..5u64 {
485 tracker.record_event(PeerChurnEvent {
486 peer_id: format!("left-{i}"),
487 event_type: ChurnEventType::Left,
488 timestamp_ms: ts(55) + i,
489 });
490 }
491
492 let m = tracker.metrics(ts(100));
493 assert_eq!(m.joins, 10);
494 assert_eq!(m.leaves, 5);
495 assert!((m.churn_rate - 0.25).abs() < 1e-9);
497 }
498
499 #[test]
500 fn test_stability_score_stable() {
501 let tracker = PeerChurnTracker::new(default_config());
502 tracker.record_event(PeerChurnEvent {
504 peer_id: "x".into(),
505 event_type: ChurnEventType::Joined,
506 timestamp_ms: ts(99),
507 });
508 let m = tracker.metrics(ts(100));
509 assert!(
511 m.stability_score > 0.9,
512 "expected score > 0.9, got {}",
513 m.stability_score
514 );
515 }
516
517 #[test]
518 fn test_stability_score_high_churn() {
519 let cfg = AdaptiveRefreshConfig {
520 window_ms: 60_000,
521 ..default_config()
522 };
523 let tracker = PeerChurnTracker::new(cfg);
524 for i in 0..300u64 {
526 tracker.record_event(PeerChurnEvent {
527 peer_id: format!("p{i}"),
528 event_type: if i % 2 == 0 {
529 ChurnEventType::Joined
530 } else {
531 ChurnEventType::Left
532 },
533 timestamp_ms: ts(50) + i,
534 });
535 }
536 let m = tracker.metrics(ts(100));
537 assert!(
539 m.stability_score < 0.5,
540 "expected score < 0.5, got {}",
541 m.stability_score
542 );
543 }
544
545 #[test]
546 fn test_recommended_interval_stable() {
547 let cfg = AdaptiveRefreshConfig {
548 window_ms: 60_000,
549 base_interval_ms: 600_000,
550 max_interval_ms: 1_800_000,
551 ..default_config()
552 };
553 let tracker = PeerChurnTracker::new(cfg.clone());
554 let interval = tracker.recommended_interval(ts(100));
556 assert!(
557 interval.as_millis() as u64 >= cfg.base_interval_ms,
558 "expected >= base, got {:?}",
559 interval
560 );
561 }
562
563 #[test]
564 fn test_recommended_interval_high_churn() {
565 let cfg = AdaptiveRefreshConfig {
566 window_ms: 10_000, high_churn_threshold: 2.0,
568 base_interval_ms: 600_000,
569 min_interval_ms: 60_000,
570 max_interval_ms: 1_800_000,
571 max_events: 10_000,
572 };
573 let tracker = PeerChurnTracker::new(cfg.clone());
574
575 for i in 0..400u64 {
578 tracker.record_event(PeerChurnEvent {
579 peer_id: format!("p{i}"),
580 event_type: ChurnEventType::Left,
581 timestamp_ms: ts(90) + i * 25, });
583 }
584
585 let interval = tracker.recommended_interval(ts(100));
586 assert_eq!(
587 interval,
588 Duration::from_millis(cfg.min_interval_ms),
589 "expected min interval under extreme churn, got {:?}",
590 interval
591 );
592 }
593
594 #[test]
599 fn test_scheduler_refresh_due() {
600 let cfg = AdaptiveRefreshConfig {
601 base_interval_ms: 1_000, min_interval_ms: 500,
603 max_interval_ms: 5_000,
604 ..default_config()
605 };
606 let scheduler = AdaptiveRefreshScheduler::new(cfg);
607 scheduler.mark_refreshed(0);
609 assert!(!scheduler.is_refresh_due(500));
611 assert!(scheduler.is_refresh_due(10_000));
613 }
614
615 #[test]
616 fn test_scheduler_not_due_after_refresh() {
617 let cfg = AdaptiveRefreshConfig {
618 base_interval_ms: 60_000,
619 min_interval_ms: 10_000,
620 max_interval_ms: 120_000,
621 ..default_config()
622 };
623 let scheduler = AdaptiveRefreshScheduler::new(cfg);
624 scheduler.mark_refreshed(ts(100));
625 assert!(!scheduler.is_refresh_due(ts(100) + 1));
627 }
628
629 #[test]
630 fn test_scheduler_refresh_count() {
631 let scheduler = AdaptiveRefreshScheduler::new(default_config());
632 assert_eq!(scheduler.refresh_count(), 0);
633 scheduler.mark_refreshed(ts(1));
634 scheduler.mark_refreshed(ts(20));
635 scheduler.mark_refreshed(ts(50));
636 assert_eq!(scheduler.refresh_count(), 3);
637 }
638
639 #[test]
644 fn test_churn_manager_peer_joined_left() {
645 let manager = ChurnResilienceManager::new(default_config());
646 manager.peer_joined("peer-1", ts(10));
647 manager.peer_left("peer-2", ts(11));
648 manager.peer_timed_out("peer-3", ts(12));
649
650 let tracker = &manager.scheduler.tracker;
652 assert_eq!(tracker.total_joins(), 1);
653 assert_eq!(tracker.total_leaves(), 2);
654 }
655
656 #[test]
657 fn test_churn_manager_should_refresh() {
658 let cfg = AdaptiveRefreshConfig {
659 base_interval_ms: 1_000,
660 min_interval_ms: 200,
661 max_interval_ms: 5_000,
662 ..default_config()
663 };
664 let manager = ChurnResilienceManager::new(cfg);
665
666 assert!(manager.should_refresh(ts(0)));
668
669 manager.mark_refreshed(ts(0));
671
672 assert!(!manager.should_refresh(ts(0) + 100));
674
675 assert!(manager.should_refresh(ts(0) + 10_000));
677 }
678
679 #[test]
684 fn test_default_config_values() {
685 let cfg = AdaptiveRefreshConfig::default();
686 assert_eq!(cfg.base_interval_ms, 10 * 60 * 1_000);
687 assert_eq!(cfg.min_interval_ms, 60 * 1_000);
688 assert_eq!(cfg.max_interval_ms, 30 * 60 * 1_000);
689 assert!((cfg.high_churn_threshold - 2.0).abs() < f64::EPSILON);
690 assert_eq!(cfg.window_ms, 60 * 1_000);
691 assert_eq!(cfg.max_events, 10_000);
692 }
693}