krabka_hlc/stamp.rs
1//! The hybrid logical clock stamp and its fixed-width byte layout.
2
3use derive_more::{Display, From, Into};
4use krabka_ids::NodeId;
5
6use crate::{error::HlcError, headers::HLC_HEADER};
7
8/// The byte length of an encoded [`Hlc`].
9///
10/// The layout is fixed-width, so this length is the same for every stamp: 8
11/// bytes of wall time, 4 bytes of counter, and 4 bytes of node id.
12pub const HLC_ENCODED_LEN: usize = 16;
13
14/// A wall-clock instant, in microseconds since the Unix epoch.
15///
16/// This is a coordinate on the time line and not an extent of time, so it is a
17/// newtype here and not a [`krabka_units::Time`]. See the code style guide's
18/// "Dimensioned Values" section for the rule.
19///
20/// The unit is the microsecond. A Kafka record timestamp counts milliseconds,
21/// and one millisecond is too coarse to order the records inside one batch. A
22/// microsecond gives the logical counter far fewer events to separate.
23///
24/// The value is signed, and a value below zero names an instant before the
25/// Unix epoch. The clock accepts such a value and does not treat it as an
26/// error, because a host whose clock is unset reports one.
27#[derive(
28 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Display, From, Into,
29)]
30pub struct WallMicros(pub i64);
31
32impl WallMicros {
33 /// The Unix epoch, `1970-01-01T00:00:00Z`.
34 pub const EPOCH: Self = Self(0);
35
36 /// The inner `i64`. Use it for arithmetic against other time counts.
37 #[must_use]
38 pub const fn get(self) -> i64 {
39 self.0
40 }
41}
42
43/// One hybrid logical clock stamp: a wall time, a logical counter, and the
44/// node that minted it.
45///
46/// The three fields give a total order over every event in the cluster.
47/// [`Ord`] compares the wall time first, then the counter, then the node id.
48/// The wall time keeps the order close to real time. The counter separates
49/// events that share a microsecond. The node id breaks the remaining ties, so
50/// two nodes that stamp the same wall time and the same counter still get one
51/// order, and every node computes that same order.
52///
53/// The type is `Copy`, and it is 16 bytes wide in memory and 16 bytes wide on
54/// the wire.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
56#[display("{wall}.{counter}@{node}")]
57pub struct Hlc {
58 /// The wall time this stamp claims.
59 pub wall: WallMicros,
60
61 /// The logical counter that separates events inside one microsecond.
62 pub counter: u32,
63
64 /// The node that minted the stamp.
65 pub node: NodeId,
66}
67
68impl Hlc {
69 /// A stamp built from its three parts.
70 #[must_use]
71 pub const fn new(wall: WallMicros, counter: u32, node: NodeId) -> Self {
72 Self {
73 wall,
74 counter,
75 node,
76 }
77 }
78
79 /// The stamp as the exact bytes of the `krabka.hlc` header value.
80 ///
81 /// The layout is big-endian and fixed-width: 8 bytes of wall time as an
82 /// `i64`, 4 bytes of counter as a `u32`, and 4 bytes of node id as a
83 /// `u32`. There is no varint framing and no length prefix. Java and Go
84 /// clients read this header by hand, and a plain big-endian layout is what
85 /// `DataInputStream.readLong` and `binary.BigEndian.Uint32` already give
86 /// them.
87 ///
88 /// # Errors
89 ///
90 /// Returns [`HlcError::NodeIdTooLarge`] when the node id is above
91 /// `u32::MAX`, which is wider than the 4-byte field the layout gives it.
92 pub fn encode(self) -> Result<[u8; HLC_ENCODED_LEN], HlcError> {
93 let node = u32::try_from(self.node.get())
94 .map_err(|_| HlcError::NodeIdTooLarge(self.node.get()))?;
95 let wall = self.wall.get().to_be_bytes();
96 let counter = self.counter.to_be_bytes();
97 let node = node.to_be_bytes();
98 Ok([
99 wall[0], wall[1], wall[2], wall[3], wall[4], wall[5], wall[6], wall[7], counter[0],
100 counter[1], counter[2], counter[3], node[0], node[1], node[2], node[3],
101 ])
102 }
103
104 /// The stamp that `bytes` carries, with full validation.
105 ///
106 /// This function reads a peer-supplied header value, so it validates the
107 /// length before it reads a field. It never panics on malformed input, and
108 /// it never allocates. Every 16-byte input is a valid stamp, because each
109 /// of the three fields covers the whole range of its type.
110 ///
111 /// Use [`extract_from_headers`](crate::extract_from_headers) on an ingress
112 /// path, where a bad value must not fail the record that carried it. Use
113 /// this function where the caller wants to know why a value failed.
114 ///
115 /// # Errors
116 ///
117 /// Returns [`HlcError::Length`] when `bytes` is not exactly
118 /// [`HLC_ENCODED_LEN`] bytes long.
119 pub fn decode(bytes: &[u8]) -> Result<Self, HlcError> {
120 let Ok(value) = <&[u8; HLC_ENCODED_LEN]>::try_from(bytes) else {
121 return Err(HlcError::Length(bytes.len()));
122 };
123 // A slice pattern over a fixed-width array. It splits the value into
124 // its three fields with no indexing, no `copy_from_slice`, and no
125 // conversion that can fail, so no path here can panic.
126 let [wall @ .., c0, c1, c2, c3, n0, n1, n2, n3] = *value;
127 Ok(Self {
128 wall: WallMicros(i64::from_be_bytes(wall)),
129 counter: u32::from_be_bytes([c0, c1, c2, c3]),
130 node: NodeId(u64::from(u32::from_be_bytes([n0, n1, n2, n3]))),
131 })
132 }
133
134 /// The stamp as a Kafka record header: the [`HLC_HEADER`] key and the
135 /// encoded value.
136 ///
137 /// A producer attaches the pair to the record it writes. A record that
138 /// carries no stamp gets no header, so an unstamped record costs zero
139 /// bytes.
140 ///
141 /// # Errors
142 ///
143 /// Returns [`HlcError::NodeIdTooLarge`] when the node id is above
144 /// `u32::MAX`. See [`Hlc::encode`].
145 pub fn header(self) -> Result<(&'static str, [u8; HLC_ENCODED_LEN]), HlcError> {
146 Ok((HLC_HEADER, self.encode()?))
147 }
148}