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