krabka_hlc/lib.rs
1//! Hybrid logical clock stamps that ride on Kafka record headers.
2//!
3//! A hybrid logical clock (HLC) gives every event in the cluster one timestamp
4//! that is close to real time and that still respects cause and effect. The
5//! stamp holds a wall time, a logical counter, and the node that minted it, so
6//! it sorts totally and every node computes the same order. Kulkarni et al.,
7//! *Logical Physical Clocks and Consistent Snapshots in Globally Distributed
8//! Databases* (OPODIS 2014), define the algorithm this crate implements.
9//!
10//! # Carriage
11//!
12//! A stamp travels on one Kafka record header, [`HLC_HEADER`]. A producer
13//! calls [`HlcClock::now`] and attaches [`Hlc::header`]. A consumer calls
14//! [`extract_from_headers`] and feeds the result to [`HlcClock::observe`].
15//!
16//! Like `krabka-trace-context`, this crate does not depend on
17//! `krabka-protocol`. It stays type-erased over `(&str, impl AsRef<[u8]>)`
18//! header pairs, so any producer or consumer header type works. The caller
19//! converts to and from its own `Header` at the edge.
20//!
21//! Four rules govern that boundary:
22//!
23//! 1. The crate never stores a peer's bytes. It parses them into an [`Hlc`],
24//! and it renders the header again from the parsed value.
25//! 2. A failure to read a stamp is silent at the carrier layer.
26//! [`extract_from_headers`] gives `None` for an absent, short, or invalid
27//! value, because a consumer that cannot read the stamp must still be able
28//! to apply the record. [`Hlc::decode`] returns a [`Result`] for a caller
29//! that wants the reason.
30//! 3. A record with no stamp carries no header, so it costs zero bytes.
31//! 4. No [`HlcError`] variant holds input bytes, so hostile input cannot reach
32//! a log line.
33//!
34//! # Byte layout
35//!
36//! The header value is 16 bytes, big-endian and fixed-width: 8 bytes of wall
37//! time as an `i64`, 4 bytes of counter as a `u32`, and 4 bytes of node id as
38//! a `u32`. There is no varint framing. Java and Go clients read this header
39//! by hand, and a plain big-endian layout is what `DataInputStream.readLong`
40//! and `binary.BigEndian.Uint32` already give them. A varint would make every
41//! such client write a decoder first.
42//!
43//! # Self-fence
44//!
45//! [`HlcClock`] refuses a stamp whose wall time is more than its configured
46//! maximum offset ahead of the local physical clock, and it holds
47//! [`DEFAULT_MAX_OFFSET`] by default. A hybrid logical clock adopts the
48//! highest wall time it sees, so one host with a broken clock would drag every
49//! node that reads its records into the future, and logical time never comes
50//! back. The fence keeps the damage inside the broken host. A stamp behind the
51//! local clock is always safe, and the clock always accepts it.
52//!
53//! # Clock-confidence signal
54//!
55//! [`HlcClock::observation`] returns what the clock has learned about skew:
56//! the current drift of the logical clock from the physical clock, how many
57//! records pulled the clock forward, how many stamps the fence refused, and
58//! the largest peer-ahead delta. A record whose stamp pulls the local clock
59//! forward by 40 milliseconds is direct evidence of 40 milliseconds of skew
60//! between this node and the node that wrote the record, measured on the data
61//! path with no probe and no extra round trip. The snapshot is a plain struct:
62//! this crate has no metrics dependency and it must not gain one.
63//!
64//! # Example
65//!
66//! ```
67//! use krabka_hlc::{HlcClock, ManualClock, WallMicros, extract_from_headers};
68//! use krabka_ids::NodeId;
69//!
70//! let physical = ManualClock::new(WallMicros(1_700_000_000_000_000));
71//!
72//! // The producer stamps a record it writes.
73//! let producer = HlcClock::with_clock(NodeId(1), &physical);
74//! let (key, value) = producer.now()?.header()?;
75//!
76//! // The consumer reads the stamp back and merges it into its own clock.
77//! let consumer = HlcClock::with_clock(NodeId(2), &physical);
78//! if let Some(stamp) = extract_from_headers([(key, value.as_slice())]) {
79//! consumer.observe(stamp)?;
80//! }
81//! # Ok::<(), krabka_hlc::HlcError>(())
82//! ```
83
84#![forbid(unsafe_code)]
85
86mod clock;
87mod error;
88mod headers;
89mod stamp;
90
91pub use self::{
92 clock::{
93 ClockObservation, DEFAULT_MAX_OFFSET, HlcClock, ManualClock, PhysicalClock, SystemClock,
94 },
95 error::HlcError,
96 headers::{HLC_HEADER, extract_from_headers},
97 stamp::{HLC_ENCODED_LEN, Hlc, WallMicros},
98};