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}