diff --git a/crates/lanspread-peer/src/services/local_monitor.rs b/crates/lanspread-peer/src/services/local_monitor.rs index c6c8746..ca00478 100644 --- a/crates/lanspread-peer/src/services/local_monitor.rs +++ b/crates/lanspread-peer/src/services/local_monitor.rs @@ -132,9 +132,18 @@ impl PollBudget { struct PollBackoff { failures: u32, retry_at: Option, + observed_root: Option, } impl PollBackoff { + fn observe_root(&mut self, root: &Path) { + if self.observed_root.as_deref() == Some(root) { + return; + } + self.observed_root = Some(root.to_path_buf()); + self.record_success(); + } + fn should_attempt(&self, now: Instant) -> bool { self.retry_at.is_none_or(|retry_at| now >= retry_at) } @@ -264,14 +273,8 @@ async fn poll_local_game_changes( previous: &mut Option, backoff: &mut PollBackoff, ) { - let configured_game_dir = ctx.game_dir.read().await; - if previous - .as_ref() - .is_some_and(|snapshot| snapshot.game_dir != *configured_game_dir) - { - backoff.record_success(); - } - drop(configured_game_dir); + let configured_game_dir = ctx.game_dir.read().await.clone(); + backoff.observe_root(&configured_game_dir); let now = Instant::now(); if !backoff.should_attempt(now) { @@ -733,6 +736,27 @@ mod tests { assert_eq!(backoff.failures, 0); } + #[test] + fn root_change_resets_backoff_only_once_until_a_snapshot_succeeds() { + let now = Instant::now(); + let mut backoff = PollBackoff::default(); + let first_root = Path::new("/first"); + let failing_root = Path::new("/oversized"); + + backoff.observe_root(first_root); + backoff.record_failure(now); + backoff.observe_root(failing_root); + assert!(backoff.should_attempt(now)); + + let delay = backoff.record_failure(now); + backoff.observe_root(failing_root); + assert!( + !backoff.should_attempt(now), + "re-observing the same failing root must preserve its backoff" + ); + assert!(backoff.should_attempt(now + delay)); + } + #[test] fn snapshot_diff_detects_game_change_and_disappearance() { let temp = TempDir::new("lanspread-local-monitor-change");