diff options
| author | tv <tv@krebsco.de> | 2026-11-06 04:34:23 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-06 04:34:23 +0100 |
| commit | d6f6c2e020ac648b54c0e024596460ffb82ca437 (patch) | |
| tree | d2b35b071b8f2f878ab333bf39d169c14015d7d1 /src | |
| parent | b4d0f3d9671c3bac8fd3c4b6c3f5f75196a06742 (diff) | |
suspend detected
Diffstat (limited to 'src')
| -rw-r--r-- | src/state/peer.rs | 12 | ||||
| -rw-r--r-- | src/swarm/mod.rs | 1 | ||||
| -rw-r--r-- | src/swarm/peer/worker.rs | 4 | ||||
| -rw-r--r-- | src/swarm/stability.rs | 9 | ||||
| -rw-r--r-- | src/swarm/suspend.rs | 93 |
5 files changed, 119 insertions, 0 deletions
diff --git a/src/state/peer.rs b/src/state/peer.rs index e0cea7e..857c29d 100644 --- a/src/state/peer.rs +++ b/src/state/peer.rs @@ -370,6 +370,18 @@ impl PeerState { .map(|(bytes, _)| bytes) } + /// The machine slept for `gap` (`swarm::suspend`): heard from, added, and awaiting + /// an answer that much earlier, as the monotonic clock stood still meanwhile. + pub(crate) fn aged(&mut self, gap: Duration) { + let earlier = |at: Instant| at.checked_sub(gap).unwrap_or(at); + self.last_seen = self.last_seen.map(earlier); + self.added_at = earlier(self.added_at); + if let Some(awaiting) = &mut self.awaiting_answer { + awaiting.ask_again_at = awaiting.ask_again_at.map(earlier); + awaiting.unreached_at = earlier(awaiting.unreached_at); + } + } + fn quiet_for(&self, now: Instant) -> Duration { now.saturating_duration_since(self.last_seen.unwrap_or(self.added_at)) } diff --git a/src/swarm/mod.rs b/src/swarm/mod.rs index 071f00e..ac62c64 100644 --- a/src/swarm/mod.rs +++ b/src/swarm/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod source; pub(crate) mod stability; pub(crate) mod state_file; pub(crate) mod stats; +pub(crate) mod suspend; pub(crate) mod sync; pub(crate) mod throughput; pub(crate) mod topology; diff --git a/src/swarm/peer/worker.rs b/src/swarm/peer/worker.rs index 7a0b502..b6f55cd 100644 --- a/src/swarm/peer/worker.rs +++ b/src/swarm/peer/worker.rs @@ -65,9 +65,13 @@ pub(crate) async fn run_peer_worker(state: AppState) { let mut last_forget_check: Option<Instant> = None; let mut forgets_applied = AppliedForgets::new(); let mut was_ready = false; + let mut awake = crate::swarm::suspend::Awake::default(); loop { state.peers_due.passing(); + if let Some(gap) = awake.check() { + crate::swarm::suspend::asleep(&state, gap).await; + } let mut next = Instant::now() + FORGET_CHECK_INTERVAL; // While joining, keep handshakes fast; dormancy would slow probes to minutes. diff --git a/src/swarm/stability.rs b/src/swarm/stability.rs index 1ee308f..8163e06 100644 --- a/src/swarm/stability.rs +++ b/src/swarm/stability.rs @@ -148,6 +148,15 @@ pub(crate) fn note_away(state: &AppState, away: Option<Duration>) { } } +/// The machine slept for `gap` (`swarm::suspend`): observed, and not connected. The +/// monotonic clock `record` goes by stopped meanwhile, so it doesn't see it. +pub(crate) fn away(state: &AppState, gap: Duration) { + let mut stability = lock(state); + if let Some(bucket) = stability.buckets.back_mut() { + bucket.observed += gap.min(WINDOW); + } +} + /// Measures, and updates our tier in the registry when it should change. Called every /// few seconds by the peer worker. pub(crate) async fn tick(state: &AppState) { diff --git a/src/swarm/suspend.rs b/src/swarm/suspend.rs new file mode 100644 index 0000000..87a13ab --- /dev/null +++ b/src/swarm/suspend.rs @@ -0,0 +1,93 @@ +//! Whether the machine was suspended (a laptop's lid closed). The monotonic clock every +//! timer here runs on (`tokio::time::Instant`: `CLOCK_MONOTONIC` on Linux, likewise +//! elsewhere) stops while the machine sleeps, so after eight hours asleep every peer +//! looks heard from minutes ago, and the hours count as connected. A clock that runs +//! on (`CLOCK_BOOTTIME`, as `/proc/uptime` gives it, on Linux; the wall clock +//! elsewhere, which a correction of the clock may move too) tells how long it was: +//! what it moved beyond the monotonic clock between two looks. The peer worker looks on +//! every pass (`Awake::check`), and on waking ages what judges silence by that much +//! (`asleep`). + +use std::time::{Duration, Instant}; + +use tracing::warn; + +use crate::state::AppState; + +/// Less than this between the clocks is no suspension (a pass that took long, a small +/// correction of the wall clock). +const AT_LEAST: Duration = Duration::from_secs(30); + +/// The clocks at the last look. +#[derive(Default)] +pub(crate) struct Awake { + last: Option<(Instant, Duration)>, +} + +impl Awake { + /// How long the machine was suspended since the last look, if at least `AT_LEAST`. + pub(crate) fn check(&mut self) -> Option<Duration> { + let now = (Instant::now(), running()?); + let (monotonic, running) = self.last.replace(now)?; + gap( + now.0.saturating_duration_since(monotonic), + now.1.saturating_sub(running), + ) + } +} + +/// How long the machine slept between two looks `monotonic` and `running` apart by +/// the two clocks, if at least `AT_LEAST`. +fn gap(monotonic: Duration, running: Duration) -> Option<Duration> { + let gap = running.saturating_sub(monotonic); + (gap >= AT_LEAST).then_some(gap) +} + +/// A clock that runs on while the machine sleeps. +fn running() -> Option<Duration> { + #[cfg(target_os = "linux")] + { + // Seconds since boot, suspension included, with two decimals. + let uptime = std::fs::read_to_string("/proc/uptime").ok()?; + let seconds: f64 = uptime.split_whitespace().next()?.parse().ok()?; + Some(Duration::from_secs_f64(seconds)) + } + #[cfg(not(target_os = "linux"))] + { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .ok() + } +} + +/// The machine slept for `gap`: peers count as silent that much longer (they are held +/// dormant, and greeted again, as after an absence that long: they may have moved on), +/// and the time counts as not connected (`swarm::stability`). +pub(crate) async fn asleep(state: &AppState, gap: Duration) { + warn!( + secs = gap.as_secs(), + "The machine was suspended; peers count as silent for that long" + ); + crate::swarm::stability::away(state, gap); + for (_, peer_state) in state.peers_snapshot().await { + let mut peer_state = peer_state.lock().await; + peer_state.aged(gap); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A gap is what the running clock moved beyond the monotonic one, from `AT_LEAST`. + #[test] + fn a_gap_between_the_clocks_is_a_suspension() { + let s = Duration::from_secs; + assert_eq!(gap(s(5), s(5)), None); + assert_eq!(gap(s(5), s(20)), None); + assert_eq!(gap(s(5), s(3605)), Some(s(3600))); + // The monotonic clock ahead (paused time in tests): none. + assert_eq!(gap(s(3600), s(5)), None); + assert!(running().is_some()); + } +} |
