aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/peer.rs12
-rw-r--r--src/swarm/mod.rs1
-rw-r--r--src/swarm/peer/worker.rs4
-rw-r--r--src/swarm/stability.rs9
-rw-r--r--src/swarm/suspend.rs93
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());
+ }
+}