Skip to main content

krabka_ids/
lib.rs

1//! Canonical newtypes for Krabka's cross-crate Kafka identifiers.
2//!
3//! A raw `i64` or `i32` carries no meaning, and Kafka's domain is full of
4//! values of the same type that are catastrophic to mix up. A log offset, a
5//! producer id, and a timestamp are all `i64`. If the code threads them as bare
6//! integers, a caller can transpose two arguments and the code still compiles.
7//! These newtypes give the recurring *cross-crate* identifiers one shared type,
8//! so the compiler rejects the mix-up. The same `Offset` then flows through the
9//! log, broker, consensus, replication, and observability crates, and no crate
10//! mints its own incompatible copy.
11//!
12//! # Wire boundary
13//!
14//! The generated Kafka wire codec, the `generated` module of
15//! `krabka-protocol`, stays raw. It must be byte-exact. Wrap a value in one of
16//! these newtypes when it enters the hand-written domain layer. Unwrap it with
17//! `.0` or [`Into`] when you write it back to a generated message or to an
18//! on-disk format. Every newtype here is `#[serde(transparent)]`, so a
19//! serialized field carries the bare inner primitive.
20//!
21//! # Comparison against raw primitives
22//!
23//! Each newtype implements `PartialEq` and `PartialOrd` against its own inner
24//! primitive in both directions. `offset >= 0`, `node_id == 7`, and
25//! `epoch == LeaderEpoch::UNKNOWN` thus read without an explicit `.0`. This
26//! applies to *comparisons only*. You still cannot pass a newtype where its
27//! primitive is expected, and you cannot pass a primitive where its newtype is
28//! expected. You cannot use a newtype as a map key of a different type, and you
29//! cannot compare it against a *different* newtype. The argument-transposition
30//! safety that motivates these types thus stays.
31//!
32//! Named constants hold the sentinel values that carry Kafka meaning:
33//! [`Offset::ZERO`], [`ProducerId::NONE`], and [`LeaderEpoch::UNKNOWN`].
34//!
35//! # Kafka `Uuid`
36//!
37//! [`KafkaUuid`] is the 128-bit cluster, topic, and log-directory id. It is not
38//! a transparent newtype: its string form, and so its serde form, is Kafka's
39//! 22-character base64url string, not the hyphenated hex of [`uuid::Uuid`].
40//!
41//! See `docs/newtype-safety-rollout.md` and the code style guide's
42//! "Newtypes for Domain Values" section.
43
44use core::{
45    cmp::Ordering,
46    ops::{Add, AddAssign, Sub},
47};
48
49use derive_more::{Display, From, Into};
50use serde::{Deserialize, Serialize};
51
52pub use self::kafka_uuid::{KafkaUuid, KafkaUuidError};
53
54mod kafka_uuid;
55
56/// A record offset within a topic partition's log. KIP wire type: `int64`.
57///
58/// The type is ordered, so it works as a `BTreeMap` key and in watermark
59/// comparisons. Advance or rewind by a count with `offset + n` or `offset - n`,
60/// where `n: i64`. The delta between two offsets is `a.0 - b.0`. The addition
61/// of two offsets has no meaning, and this crate does not implement it.
62#[derive(
63    Debug,
64    Clone,
65    Copy,
66    Default,
67    PartialEq,
68    Eq,
69    PartialOrd,
70    Ord,
71    Hash,
72    Display,
73    From,
74    Into,
75    Serialize,
76    Deserialize,
77)]
78#[serde(transparent)]
79pub struct Offset(pub i64);
80
81impl Offset {
82    /// The log's first offset. It is also the initial log-start offset and the
83    /// high-watermark of an empty partition.
84    pub const ZERO: Self = Offset(0);
85
86    /// The inner `i64`. Use it at the wire and generated boundary, and for
87    /// arithmetic against other integer quantities.
88    #[must_use]
89    pub const fn get(self) -> i64 {
90        self.0
91    }
92}
93
94impl Add<i64> for Offset {
95    type Output = Offset;
96    fn add(self, count: i64) -> Offset {
97        Offset(self.0 + count)
98    }
99}
100
101impl Sub<i64> for Offset {
102    type Output = Offset;
103    fn sub(self, count: i64) -> Offset {
104        Offset(self.0 - count)
105    }
106}
107
108impl AddAssign<i64> for Offset {
109    fn add_assign(&mut self, count: i64) {
110        self.0 += count;
111    }
112}
113
114/// A partition index within a topic. KIP wire type: `int32`.
115///
116/// The type is ordered and hashable, so it keys the per-partition maps in the
117/// broker and replication paths. A partition index is an identifier, not a
118/// quantity, so this crate implements no arithmetic.
119#[derive(
120    Debug,
121    Clone,
122    Copy,
123    Default,
124    PartialEq,
125    Eq,
126    PartialOrd,
127    Ord,
128    Hash,
129    Display,
130    From,
131    Into,
132    Serialize,
133    Deserialize,
134)]
135#[serde(transparent)]
136pub struct PartitionIndex(pub i32);
137
138impl PartitionIndex {
139    /// The inner `i32`. Use it at the wire and generated boundary.
140    #[must_use]
141    pub const fn get(self) -> i32 {
142        self.0
143    }
144}
145
146/// A broker, node, replica, or controller identifier.
147///
148/// Krabka carries this value as a `u64` internally: the consensus peer id, the
149/// KIP-853 voter id, and the partition replica, leader, and ISR ids. On the
150/// Kafka wire the same value is an `int32`: `broker.id`, `node.id`, and the
151/// replica ids. The protocol boundary converts between the two. The value is an
152/// identifier, not a quantity, so this crate supplies no arithmetic.
153#[derive(
154    Debug,
155    Clone,
156    Copy,
157    Default,
158    PartialEq,
159    Eq,
160    PartialOrd,
161    Ord,
162    Hash,
163    Display,
164    From,
165    Into,
166    Serialize,
167    Deserialize,
168)]
169#[serde(transparent)]
170pub struct NodeId(pub u64);
171
172impl NodeId {
173    /// The inner `u64`. Use it at the wire and generated boundary.
174    #[must_use]
175    pub const fn get(self) -> u64 {
176        self.0
177    }
178}
179
180/// A Kafka idempotent or transactional producer id. KIP-98 wire type: `int64`.
181///
182/// The id identifies a producer session across the broker's producer-state and
183/// transaction paths and across the log's aborted-transaction index. `-1`, the
184/// `NO_PRODUCER_ID` sentinel, is valid and round-trips as the inner `i64`. The
185/// id is an identifier, not a quantity, so this crate supplies no arithmetic.
186#[derive(
187    Debug,
188    Clone,
189    Copy,
190    Default,
191    PartialEq,
192    Eq,
193    PartialOrd,
194    Ord,
195    Hash,
196    Display,
197    From,
198    Into,
199    Serialize,
200    Deserialize,
201)]
202#[serde(transparent)]
203pub struct ProducerId(pub i64);
204
205impl ProducerId {
206    /// KIP-98 `NO_PRODUCER_ID` (`-1`): no idempotent or transactional producer
207    /// is assigned.
208    pub const NONE: Self = ProducerId(-1);
209
210    /// The inner `i64`. Use it at the wire and generated boundary.
211    #[must_use]
212    pub const fn get(self) -> i64 {
213        self.0
214    }
215
216    /// Whether this is the [`ProducerId::NONE`] sentinel, which means that no
217    /// producer is assigned.
218    #[must_use]
219    pub const fn is_none(self) -> bool {
220        self.0 == Self::NONE.0
221    }
222}
223
224/// A partition leader epoch. KIP-320 wire type: `int32`.
225///
226/// This is a monotonic per-partition counter, and every leader change
227/// increments it. It fences stale leaders and it bounds follower log
228/// truncation. `-1`, the `UNKNOWN_LEADER_EPOCH` sentinel, is a valid wire value
229/// and round-trips as the inner `i32`. The type is ordered, so epochs compare
230/// directly. Advance to the next epoch with [`LeaderEpoch::next`].
231///
232/// Note: the deterministic consensus core, `krabka-kraft-core`, tracks its own
233/// always-non-negative epoch as a `u32`, `krabka_kraft_core::types::Epoch`.
234/// `krabka-raft` converts to and from this wire type at the controller
235/// boundary.
236#[derive(
237    Debug,
238    Clone,
239    Copy,
240    Default,
241    PartialEq,
242    Eq,
243    PartialOrd,
244    Ord,
245    Hash,
246    Display,
247    From,
248    Into,
249    Serialize,
250    Deserialize,
251)]
252#[serde(transparent)]
253pub struct LeaderEpoch(pub i32);
254
255impl LeaderEpoch {
256    /// KIP-320 `UNKNOWN_LEADER_EPOCH` (`-1`): the leader epoch is unknown or
257    /// unset. Examples are an older client, and a partition with no elected
258    /// leader yet.
259    pub const UNKNOWN: Self = LeaderEpoch(-1);
260
261    /// The epoch a partition's first leader starts at (`0`).
262    pub const INITIAL: Self = LeaderEpoch(0);
263
264    /// The inner `i32`. Use it at the wire and generated boundary.
265    #[must_use]
266    pub const fn get(self) -> i32 {
267        self.0
268    }
269
270    /// Whether this is a real epoch rather than the
271    /// [`UNKNOWN`](LeaderEpoch::UNKNOWN) sentinel.
272    #[must_use]
273    pub const fn is_known(self) -> bool {
274        self.0 >= 0
275    }
276
277    /// The next epoch after this one. A leader change increments the epoch by
278    /// one.
279    #[must_use]
280    pub const fn next(self) -> Self {
281        LeaderEpoch(self.0 + 1)
282    }
283}
284
285/// A Kafka request API key, as the raw wire `int16`.
286///
287/// This is the numeric code in the `ApiKey` field of a request header. It is
288/// different from the typed `krabka_protocol::ApiKey` *enum*, which names each
289/// key, such as `Produce` and `Fetch`. This newtype is the boundary value that
290/// goes through the hand-written header construction and through the tap and
291/// proxy frame parsers. It pairs with an [`ApiVersion`], and the two are
292/// adjacent `i16`s that must not be transposed.
293#[derive(
294    Debug,
295    Clone,
296    Copy,
297    Default,
298    PartialEq,
299    Eq,
300    PartialOrd,
301    Ord,
302    Hash,
303    Display,
304    From,
305    Into,
306    Serialize,
307    Deserialize,
308)]
309#[serde(transparent)]
310pub struct ApiKey(pub i16);
311
312impl ApiKey {
313    /// The inner `i16`. Use it at the wire and generated boundary.
314    #[must_use]
315    pub const fn get(self) -> i16 {
316        self.0
317    }
318}
319
320/// A Kafka request and response API version. Wire type: `int16`.
321///
322/// It pairs with an [`ApiKey`] in a request header. The two adjacent `i16`s are
323/// the textbook swap shape, and these distinct types prevent that swap.
324#[derive(
325    Debug,
326    Clone,
327    Copy,
328    Default,
329    PartialEq,
330    Eq,
331    PartialOrd,
332    Ord,
333    Hash,
334    Display,
335    From,
336    Into,
337    Serialize,
338    Deserialize,
339)]
340#[serde(transparent)]
341pub struct ApiVersion(pub i16);
342
343impl ApiVersion {
344    /// The inner `i16`. Use it at the wire and generated boundary.
345    #[must_use]
346    pub const fn get(self) -> i16 {
347        self.0
348    }
349}
350
351/// Generates cross-type comparison impls in both directions, so the code can
352/// compare a domain newtype directly against its raw inner primitive without an
353/// explicit `.0`. Examples are `offset >= 0` and `node_id == 7`. The macro is
354/// scoped to comparisons, not to argument passing and not to map keys. See the
355/// module docs.
356macro_rules! impl_primitive_cmp {
357    ($ty:ty, $inner:ty) => {
358        impl PartialEq<$inner> for $ty {
359            #[inline]
360            fn eq(&self, other: &$inner) -> bool {
361                self.0 == *other
362            }
363        }
364        impl PartialEq<$ty> for $inner {
365            #[inline]
366            fn eq(&self, other: &$ty) -> bool {
367                *self == other.0
368            }
369        }
370        impl PartialOrd<$inner> for $ty {
371            #[inline]
372            fn partial_cmp(&self, other: &$inner) -> Option<Ordering> {
373                self.0.partial_cmp(other)
374            }
375        }
376        impl PartialOrd<$ty> for $inner {
377            #[inline]
378            fn partial_cmp(&self, other: &$ty) -> Option<Ordering> {
379                self.partial_cmp(&other.0)
380            }
381        }
382    };
383}
384
385impl_primitive_cmp!(Offset, i64);
386impl_primitive_cmp!(PartitionIndex, i32);
387impl_primitive_cmp!(NodeId, u64);
388impl_primitive_cmp!(ProducerId, i64);
389impl_primitive_cmp!(LeaderEpoch, i32);
390impl_primitive_cmp!(ApiKey, i16);
391impl_primitive_cmp!(ApiVersion, i16);
392
393#[cfg(test)]
394mod tests {
395    use super::{ApiKey, ApiVersion, LeaderEpoch, NodeId, Offset, PartitionIndex, ProducerId};
396
397    /// `Offset` carries the three arithmetic impls the log uses to walk a
398    /// partition, and none of them had a test. The operands below are chosen so
399    /// that every replacement lands somewhere else: with 10 and 3, `+` gives 13
400    /// where `*` gives 30, `-` gives 7 and a defaulted return gives 0, and the
401    /// same three values separate `-` from `+` and `/`. `add_assign` is checked
402    /// against the value it started from, which is what a body replaced by `()`
403    /// would leave behind.
404    #[test]
405    fn offset_arithmetic_moves_by_the_operand() {
406        assert2::check!(Offset(10) + 3 == Offset(13));
407        assert2::check!(Offset(10) - 3 == Offset(7));
408
409        let mut offset = Offset(10);
410        offset += 3;
411        assert2::check!(offset == Offset(13));
412
413        // Walking backwards past a positive value, and across zero, is what the
414        // log start does after a retention delete.
415        assert2::check!(Offset(2) - 5 == Offset(-3));
416        assert2::check!(Offset::ZERO + 1 == Offset(1));
417    }
418
419    #[test]
420    fn accessors_and_advance_return_the_inner_value() {
421        assert2::assert!(Offset(9).get() == 9);
422        assert2::assert!(PartitionIndex(4).get() == 4);
423        assert2::assert!(NodeId(7).get() == 7);
424        assert2::assert!(ProducerId(42).get() == 42);
425        assert2::assert!(LeaderEpoch(3).get() == 3);
426        assert2::assert!(ApiKey(18).get() == 18);
427        assert2::assert!(ApiVersion(2).get() == 2);
428        assert2::assert!(LeaderEpoch(4).next() == LeaderEpoch(5));
429    }
430
431    #[test]
432    fn compares_against_raw_primitive_in_both_directions() {
433        assert2::assert!(Offset(5) == 5);
434        assert2::assert!(5 == Offset(5));
435        assert2::assert!(Offset(5) != 6);
436        assert2::assert!(Offset(5) > 3);
437        assert2::assert!(3 < Offset(5));
438        assert2::assert!(Offset::ZERO >= 0);
439        assert2::assert!(NodeId(7) == 7);
440        assert2::assert!(ApiKey(18) == 18);
441    }
442
443    #[test]
444    fn newtype_to_newtype_comparison_still_holds() {
445        assert2::assert!(Offset(1) < Offset(2));
446        assert2::assert!(Offset(2) == Offset(2));
447        let mut xs = [Offset(3), Offset(1), Offset(2)];
448        xs.sort();
449        assert2::assert!(xs == [Offset(1), Offset(2), Offset(3)]);
450    }
451
452    #[test]
453    fn sentinels_carry_kafka_meaning() {
454        assert2::assert!(LeaderEpoch::UNKNOWN == -1);
455        assert2::assert!(!LeaderEpoch::UNKNOWN.is_known());
456        assert2::assert!(LeaderEpoch::INITIAL == 0);
457        assert2::assert!(LeaderEpoch::INITIAL.is_known());
458        assert2::assert!(ProducerId::NONE == -1);
459        assert2::assert!(ProducerId::NONE.is_none());
460        assert2::assert!(!ProducerId(0).is_none());
461        assert2::assert!(Offset::ZERO == 0);
462    }
463}