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}