aboutsummaryrefslogtreecommitdiffstats
path: root/src/swarm/suspend.rs
blob: 87a13ab429c93b686d319a487f303d89edfb541d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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());
    }
}