Skip to main content

git_policy_cli/
claim_time.rs

1//! What:
2//!  A reading of the clocks a claim's lease is judged by: the boot the host is in, the host's monotonic clock, and the wall clock.
3//!  Two readings of one boot are compared by the monotonic clock; readings of different boots by the wall clock.
4//! Why:
5//!  A lease must run out when its holder stopped, not when somebody sets the date. The wall clock steps (a time sync, a person,
6//!  a virtual machine resuming), and a step forward made a live holder look an hour stale while a step back kept a dead
7//!  holder's claim alive. `CLOCK_MONOTONIC` is one clock for every process of a host, never steps, and does not count a
8//!  suspended host's sleep, so a holder and a recoverer on one boot agree on how long a claim went unrenewed. It restarts at
9//!  every boot, so a reading carries the boot ID (`/proc/sys/kernel/random/boot_id`); across boots, or where the platform has
10//!  neither, only the wall clock is left to compare and it is used as the fallback.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const stamp = { bootId: readBootId(), monotonicMillis: hrtimeMillis(), wallSeconds: Date.now() / 1000 };
15//! const elapsed = (now, then) => now.bootId === then.bootId ? now.monotonicMillis - then.monotonicMillis : (now.wallSeconds - then.wallSeconds) * 1000;
16//! ```
17
18use super::service_replay::capture_stamp;
19use std::sync::OnceLock;
20use std::time::{Duration, SystemTime, UNIX_EPOCH};
21
22/// Separates the three parts of a reading when it is written into a claim's file name.
23const FIELD_SEPARATOR: char = '~';
24
25/// The length of a boot ID without its dashes.
26const BOOT_ID_LENGTH: usize = 32;
27
28/// What:
29///  One reading of the three clocks.
30/// Why:
31///   A claim carries the reading taken when it was last renewed; whoever judges the claim takes a reading of its own.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// type ClaimTime = { bootId: string; monotonicMillis: number; wallSeconds: number };
36/// ```
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ClaimTime {
39    /// The boot the monotonic reading belongs to; empty where the host has no boot ID or no monotonic clock.
40    boot_id: String,
41    /// Milliseconds of `CLOCK_MONOTONIC`.
42    monotonic_millis: u64,
43    /// Seconds since the Unix epoch of the wall clock.
44    wall_seconds: u64,
45}
46
47/// The boot ID of this host's current boot, without dashes, or nothing when it cannot be read.
48fn current_boot_id() -> &'static str {
49    static BOOT_ID: OnceLock<String> = OnceLock::new();
50    return BOOT_ID
51        .get_or_init(|| {
52            let Ok(text) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") else {
53                return String::new();
54            };
55            let compact: String = text.trim().chars().filter(|character| { return *character != '-'; }).collect();
56            return if compact.len() == BOOT_ID_LENGTH && compact.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) { compact } else { String::new() };
57        })
58        .as_str();
59}
60
61/// Methods over a reading.
62impl ClaimTime {
63    /// A reading of the clocks now.
64    pub fn read() -> ClaimTime {
65        let monotonic_millis: u64 = capture_stamp() / 1_000_000;
66        // Without a monotonic clock the boot ID would pair with a meaningless number, so the reading has none.
67        let boot_id: String = if monotonic_millis == 0 { String::new() } else { String::from(current_boot_id()) };
68        let wall_seconds: u64 = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); });
69        return ClaimTime { boot_id, monotonic_millis, wall_seconds };
70    }
71
72    /// A reading of the given parts, which is how a test says what the clocks showed.
73    pub fn from_parts(boot_id: &str, monotonic: Duration, wall: Duration) -> ClaimTime {
74        return ClaimTime {
75            boot_id: boot_id.to_owned(),
76            monotonic_millis: u64::try_from(monotonic.as_millis()).unwrap_or(u64::MAX),
77            wall_seconds: wall.as_secs(),
78        };
79    }
80
81    /// The reading `by` later on both clocks, which is time passing.
82    pub fn advanced(&self, by: Duration) -> ClaimTime {
83        return ClaimTime {
84            boot_id: self.boot_id.clone(),
85            monotonic_millis: self.monotonic_millis.saturating_add(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
86            wall_seconds: self.wall_seconds.saturating_add(by.as_secs()),
87        };
88    }
89
90    /// The reading `by` earlier on both clocks, which is how a test makes a claim whose holder last renewed that long ago.
91    pub fn rewound(&self, by: Duration) -> ClaimTime {
92        return ClaimTime {
93            boot_id: self.boot_id.clone(),
94            monotonic_millis: self.monotonic_millis.saturating_sub(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
95            wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()),
96        };
97    }
98
99    /// The reading with the wall clock set `by` forward and the monotonic clock untouched, which is a clock step.
100    pub fn wall_stepped_forward(&self, by: Duration) -> ClaimTime {
101        return ClaimTime { wall_seconds: self.wall_seconds.saturating_add(by.as_secs()), ..self.clone() };
102    }
103
104    /// The reading with the wall clock set `by` back and the monotonic clock untouched, which is a clock step.
105    pub fn wall_stepped_back(&self, by: Duration) -> ClaimTime {
106        return ClaimTime { wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()), ..self.clone() };
107    }
108
109    /// The reading in another boot, which is a restart of the host; the wall clock is what is left to compare.
110    pub fn in_boot(&self, boot_id: &str) -> ClaimTime {
111        return ClaimTime { boot_id: boot_id.to_owned(), ..self.clone() };
112    }
113
114    /// The reading a wall-clock time stands for where there is nothing else, such as the modification time of a claim an
115    /// earlier release stamped.
116    pub fn from_wall(wall: SystemTime) -> ClaimTime {
117        return ClaimTime {
118            boot_id: String::new(),
119            monotonic_millis: 0,
120            wall_seconds: wall.duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); }),
121        };
122    }
123
124    /// How long ago `earlier` was, by the clock the two readings share: the monotonic clock within one boot, the wall clock
125    /// across boots. A reading from the future of this one counts as no time at all.
126    pub fn elapsed_since(&self, earlier: &ClaimTime) -> Duration {
127        if !self.boot_id.is_empty() && self.boot_id == earlier.boot_id {
128            return Duration::from_millis(self.monotonic_millis.saturating_sub(earlier.monotonic_millis));
129        }
130        return Duration::from_secs(self.wall_seconds.saturating_sub(earlier.wall_seconds));
131    }
132
133    /// The reading as the three file-name parts `boot~monotonic~wall`.
134    pub fn encode(&self) -> String {
135        return format!("{}{FIELD_SEPARATOR}{}{FIELD_SEPARATOR}{}", self.boot_id, self.monotonic_millis, self.wall_seconds);
136    }
137
138    /// The reading `encode` wrote, or nothing for any other text.
139    pub fn decode(text: &str) -> Option<ClaimTime> {
140        let mut parts = text.split(FIELD_SEPARATOR);
141        let boot_id: &str = parts.next()?;
142        let monotonic_millis: u64 = parts.next()?.parse().ok()?;
143        let wall_seconds: u64 = parts.next()?.parse().ok()?;
144        if parts.next().is_some() || !(boot_id.is_empty() || (boot_id.len() == BOOT_ID_LENGTH && boot_id.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }))) {
145            return None;
146        }
147        return Some(ClaimTime { boot_id: boot_id.to_owned(), monotonic_millis, wall_seconds });
148    }
149}
150
151/// Clock controls stay out of the release executable.
152#[cfg(test)]
153#[path = "claim_time_tests.rs"]
154mod tests;