hlc_gen/
timestamp.rs

1use {
2    crate::{
3        epoch::{CUSTOM_EPOCH, CustomEpochTimestamp},
4        error::{HlcError, HlcResult},
5    },
6    chrono::Utc,
7    std::sync::atomic::{AtomicU64, Ordering},
8};
9
10/// Number of bits to represent physical time in milliseconds since custom
11/// epoch.
12static PT_BITS: u8 = 42;
13
14/// Maximum value for physical time.
15static PT_MAX: u64 = (1 << PT_BITS) - 1;
16
17/// Number of bits to represent logical clock counter.
18static LC_BITS: u8 = 22;
19
20/// Maximum value for logical clock.
21static LC_MAX: u64 = (1 << LC_BITS) - 1;
22
23/// Hybrid logical clock (HLC) timestamp.
24///
25/// This is a lock-free implementation of a hybrid logical clock (HLC)
26/// timestamp.
27///
28/// The timestamp is represented as a 64-bit unsigned integer. The upper 42 bits
29/// represent the physical time in milliseconds since a custom epoch, and the
30/// lower 22 bits represent the logical clock count.
31///
32/// Normally, you don't need to worry about the details of the representation.
33///
34/// Whenever you need to create a new timestamp, use the
35/// [`new()`](Self::new()) to create a timestamp with the current time,
36/// or [`from_parts()`](Self::from_parts()) to create a timestamp with a
37/// specific Unix timestamp (in ms) and logical clock count.
38///
39/// When you need to update the timestamp, use the [`update()`](Self::update())
40/// method.
41///
42/// Finally, you can use the [`as_u64()`](Self::as_u64()) method to get the raw
43/// data, which is guaranteed to be monotonically increasing and capturing the
44/// happens-before relationship.
45///
46/// To get the physical time and logical clock count, use the
47/// [`timestamp()`](Self::timestamp()) and [`count()`](Self::count()) methods
48/// respectively.
49#[derive(Debug)]
50pub struct HlcTimestamp(AtomicU64);
51
52impl Default for HlcTimestamp {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl TryFrom<u64> for HlcTimestamp {
59    type Error = HlcError;
60
61    fn try_from(value: u64) -> Result<Self, Self::Error> {
62        let pt = (value >> LC_BITS) & PT_MAX;
63        let lc = value & LC_MAX;
64        Self::from_parts(CustomEpochTimestamp::to_unix_timestamp(pt), lc)
65    }
66}
67
68impl Clone for HlcTimestamp {
69    fn clone(&self) -> Self {
70        let current_value = self.0.load(Ordering::Acquire);
71        // Since there are no possible way to create invalid HLC timestamp (internal
72        // data is not exposed and update method returns error on invalid values), this
73        // conversion is infallible.
74        current_value
75            .try_into()
76            .expect("Failed to clone HLC timestamp")
77    }
78}
79
80impl PartialEq for HlcTimestamp {
81    fn eq(&self, other: &Self) -> bool {
82        self.as_u64() == other.as_u64()
83    }
84}
85
86impl Eq for HlcTimestamp {}
87
88impl PartialOrd for HlcTimestamp {
89    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
90        Some(self.as_u64().cmp(&other.as_u64()))
91    }
92}
93
94impl Ord for HlcTimestamp {
95    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
96        self.as_u64().cmp(&other.as_u64())
97    }
98}
99
100impl HlcTimestamp {
101    /// Creates a new HLC timestamp.
102    pub fn new() -> Self {
103        let unix_timestamp = Utc::now().timestamp_millis();
104        let epoch_timestamp = (unix_timestamp - CUSTOM_EPOCH) as u64;
105        Self(AtomicU64::new(epoch_timestamp << LC_BITS))
106    }
107
108    /// Creates a new HLC timestamp from the given physical time and logical
109    /// clock count.
110    pub fn from_parts(pt: i64, lc: u64) -> HlcResult<Self> {
111        if pt > PT_MAX as i64 {
112            return Err(HlcError::PhysicalTimeExceedsMax(pt, PT_MAX));
113        }
114        if lc > LC_MAX {
115            return Err(HlcError::LogicalClockExceedsMax(lc, LC_MAX));
116        }
117
118        // Convert the physical time to milliseconds since the custom epoch.
119        let ts = CustomEpochTimestamp::from_unix_timestamp(pt)?;
120
121        let combined = (ts.millis() << LC_BITS) | lc;
122        Ok(Self(AtomicU64::new(combined)))
123    }
124
125    /// Sets the physical time and logical clock count.
126    ///
127    /// Expected closure gets the current physical time and logical clock count
128    /// at the moment of the call and must return the new values for both.
129    ///
130    /// This is an atomic operation that ensures thread safety in a lock-free
131    /// fashion. Either both values are updated or none are.
132    pub fn update<F>(&self, new_values: F) -> HlcResult<HlcTimestamp>
133    where
134        F: Fn(i64, u64) -> HlcResult<(i64, u64)>,
135    {
136        loop {
137            let current = self.0.load(Ordering::Acquire);
138
139            // Obtain new values for physical time and logical clock count.
140            let (pt, lc) = new_values(
141                CustomEpochTimestamp::to_unix_timestamp((current >> LC_BITS) & PT_MAX),
142                current & LC_MAX,
143            )?;
144
145            if pt > PT_MAX as i64 {
146                return Err(HlcError::PhysicalTimeExceedsMax(pt, PT_MAX));
147            }
148            if lc > LC_MAX {
149                return Err(HlcError::LogicalClockExceedsMax(lc, LC_MAX));
150            }
151
152            let ts = CustomEpochTimestamp::from_unix_timestamp(pt)?;
153            let new_combined = (ts.millis() << LC_BITS) | lc;
154
155            if self
156                .0
157                .compare_exchange(current, new_combined, Ordering::AcqRel, Ordering::Acquire)
158                .is_ok()
159            {
160                return Ok(HlcTimestamp(AtomicU64::new(new_combined)));
161            }
162        }
163    }
164
165    /// Returns the current HLC timestamp as a number.
166    pub fn as_u64(&self) -> u64 {
167        self.0.load(Ordering::Acquire)
168    }
169
170    /// Creates a new HLC timestamp from the given u64 value.
171    ///
172    /// The encoded value should be in the format of the HLC timestamp.
173    pub fn from_u64(value: u64) -> HlcResult<Self> {
174        value.try_into()
175    }
176
177    /// Returns the current physical timestamp and logical clock count as a
178    /// tuple.
179    ///
180    /// This operation is atomic, as it uses a single load operation to get the
181    /// inner value.
182    pub fn parts(&self) -> (i64, u64) {
183        let raw_value = self.as_u64();
184        let pt = (raw_value >> LC_BITS) & PT_MAX;
185        let lc = raw_value & LC_MAX;
186        (CustomEpochTimestamp::to_unix_timestamp(pt), lc)
187    }
188}