Skip to main content

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