Skip to main content

p2panda_net/
timestamp.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Logical and wall-clock timestamps and hybrids to determine order of events.
4use std::fmt::Display;
5use std::hash::Hash as StdHash;
6use std::num::ParseIntError;
7use std::ops::Add;
8use std::str::FromStr;
9#[cfg(not(test))]
10use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
11
12#[cfg(test)]
13use mock_instant::SystemTimeError;
14#[cfg(test)]
15use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19/// Microseconds since the UNIX epoch based on system time.
20///
21/// This is using microseconds instead leap seconds for larger precision (unlike standard UNIX
22/// timestamps).
23#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StdHash, Serialize, Deserialize)]
24pub struct Timestamp(u64);
25
26impl Timestamp {
27    #[cfg(test)]
28    pub fn new(value: u64) -> Self {
29        Self(value)
30    }
31
32    pub fn now() -> Self {
33        let now = SystemTime::now();
34        now.try_into().expect("system time went backwards")
35    }
36}
37
38impl From<Timestamp> for u64 {
39    fn from(value: Timestamp) -> Self {
40        value.0
41    }
42}
43
44#[cfg(test)]
45impl From<u64> for Timestamp {
46    fn from(value: u64) -> Self {
47        Self(value)
48    }
49}
50
51impl TryFrom<SystemTime> for Timestamp {
52    type Error = SystemTimeError;
53
54    fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
55        let duration = system_time.duration_since(UNIX_EPOCH)?;
56        // Use microseconds precision instead of seconds unlike standard UNIX timestamps.
57        Ok(Self(duration.as_micros() as u64))
58    }
59}
60
61impl Display for Timestamp {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(f, "{}", self.0)
64    }
65}
66
67/// Logical clock algorithm to determine the order of events.
68///
69/// <https://en.wikipedia.org/wiki/Lamport_timestamp>
70#[derive(
71    Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, StdHash, Serialize, Deserialize,
72)]
73pub struct LamportTimestamp(u64);
74
75impl LamportTimestamp {
76    pub fn new(value: u64) -> Self {
77        Self(value)
78    }
79
80    pub fn increment(self) -> Self {
81        Self(self.0 + 1)
82    }
83}
84
85impl Display for LamportTimestamp {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91impl Add<u64> for LamportTimestamp {
92    type Output = LamportTimestamp;
93
94    fn add(self, rhs: u64) -> Self::Output {
95        Self(self.0.saturating_add(rhs))
96    }
97}
98
99impl From<u64> for LamportTimestamp {
100    fn from(value: u64) -> Self {
101        Self(value)
102    }
103}
104
105/// Hybrid UNIX and logical clock timestamp.
106///
107/// This allows for settings where we want the guarantees of a monotonically incrementing lamport
108/// timestamp but still "move forwards" with "global time" so we get the best of both worlds during
109/// ordering:
110///
111/// * If we lost the state of our logical clock we will still be _after_ previous timestamps, as
112///   the global UNIX time advanced (given that no OS clock was faulty).
113/// * If the UNIX timestamp is the same we know which item came after because of the logical clock
114///   and don't need to rely on more "random" tie-breakers, like a hashing digest.
115#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StdHash, Serialize, Deserialize)]
116pub struct HybridTimestamp(Timestamp, LamportTimestamp);
117
118impl HybridTimestamp {
119    pub fn from_parts(timestamp: Timestamp, logical: LamportTimestamp) -> Self {
120        Self(timestamp, logical)
121    }
122
123    pub fn now() -> Self {
124        Self(Timestamp::now(), LamportTimestamp::default())
125    }
126
127    pub fn increment(self) -> Self {
128        let timestamp = Timestamp::now();
129        if timestamp == self.0 {
130            Self(timestamp, self.1.increment())
131        } else {
132            Self(timestamp, LamportTimestamp::default())
133        }
134    }
135}
136
137const SEPARATOR: char = '/';
138
139impl FromStr for HybridTimestamp {
140    type Err = HybridTimestampError;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        let parts: Vec<_> = s.split(SEPARATOR).collect();
144        if parts.len() != 2 {
145            return Err(HybridTimestampError::Size(parts.len()));
146        }
147
148        let unix: u64 = u64::from_str(parts[0]).map_err(HybridTimestampError::ParseInt)?;
149        let logical: u64 = u64::from_str(parts[1]).map_err(HybridTimestampError::ParseInt)?;
150
151        Ok(Self(Timestamp(unix), LamportTimestamp::new(logical)))
152    }
153}
154
155impl Display for HybridTimestamp {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{}{SEPARATOR}{}", self.0, self.1)
158    }
159}
160
161#[cfg(test)]
162impl From<u64> for HybridTimestamp {
163    fn from(value: u64) -> Self {
164        Self(Timestamp::new(value), LamportTimestamp::default())
165    }
166}
167
168#[derive(Debug, Error)]
169pub enum HybridTimestampError {
170    #[error("invalid size, expected 2, given: {0}")]
171    Size(usize),
172
173    #[error(transparent)]
174    ParseInt(#[from] ParseIntError),
175}
176
177#[cfg(test)]
178mod tests {
179    use std::str::FromStr;
180    use std::time::Duration;
181
182    use mock_instant::thread_local::MockClock;
183
184    use super::{HybridTimestamp, LamportTimestamp};
185
186    #[test]
187    fn convert_and_compare() {
188        assert!(LamportTimestamp(5) > 3.into());
189    }
190
191    #[test]
192    fn add_u64_with_max() {
193        assert_eq!(LamportTimestamp(3) + 3u64, LamportTimestamp(6));
194        assert_eq!(
195            LamportTimestamp(u64::MAX) + 3u64,
196            LamportTimestamp(u64::MAX)
197        );
198    }
199
200    #[test]
201    fn increment_hybrid() {
202        MockClock::set_system_time(Duration::from_secs(0));
203
204        let timestamp_1 = HybridTimestamp::now();
205        let timestamp_2 = timestamp_1.increment();
206        assert!(timestamp_2 > timestamp_1);
207
208        MockClock::advance_system_time(Duration::from_secs(1));
209
210        let timestamp_3 = HybridTimestamp::now();
211        let timestamp_4 = timestamp_3.increment();
212        assert!(timestamp_3 > timestamp_2);
213        assert!(timestamp_4 > timestamp_3);
214
215        MockClock::advance_system_time(Duration::from_secs(1));
216
217        let timestamp_5 = HybridTimestamp::now();
218        let timestamp_6 = HybridTimestamp::now();
219
220        assert!(timestamp_5 > timestamp_4);
221        assert!(timestamp_6 > timestamp_4);
222        assert_eq!(timestamp_5, timestamp_6);
223    }
224
225    #[test]
226    fn hybrid_from_str() {
227        let timestamp = HybridTimestamp::now().increment().increment();
228        let timestamp_str = timestamp.to_string();
229        assert_eq!(
230            HybridTimestamp::from_str(&timestamp_str).unwrap(),
231            timestamp
232        );
233    }
234}