Skip to main content

krabka_client_coordination/
record.rs

1//! The frozen `__coordination_state` record layouts and their codec.
2//!
3//! A member of a coordination role writes its registration and its lease to the
4//! compacted internal topic [`COORDINATION_STATE_TOPIC`]. The key carries the
5//! record kind, so one topic holds both kinds. The key is also the compaction
6//! key, so the topic keeps the last record of every role and member. A record
7//! with a null value is a tombstone. A tombstone of kind 0 deregisters one
8//! member, and a tombstone of kind 1 clears the lease of one role.
9//!
10//! The layouts below are frozen. `krabka-streams-java` and `krabka-streams-go`
11//! re-implement them byte for byte, and all three projects assert the same
12//! golden bytes. Do not change a field, a field order, or a version number
13//! without the same change in the two ports.
14//!
15//! ```text
16//! key:
17//!   version  i16 = 0
18//!   kind     i16          0 registration, 1 lease
19//!   role     string
20//!   member   string       the member id for kind 0, the empty string for kind 1
21//!
22//! registration value:
23//!   version        i16 = 0
24//!   member         string
25//!   registered_at  i64    epoch milliseconds
26//!
27//! lease value:
28//!   version         i16 = 0
29//!   member          string
30//!   producer_id     i64
31//!   producer_epoch  i16
32//!   granted_at      i64   epoch milliseconds
33//!   deadline        i64   epoch milliseconds
34//! ```
35//!
36//! Every integer is big-endian and signed. A string is an `i16` byte length and
37//! then plain UTF-8 bytes. This is Kafka's own native string layout. It is not
38//! Java's modified UTF-8, which `DataOutput::writeUTF` produces. The length is
39//! never negative, because the format has no null string. An absent member is
40//! the empty string, and it encodes as the two bytes `00 00`.
41
42use std::{fmt, str, str::FromStr};
43
44use bytes::{BufMut, Bytes, BytesMut};
45
46/// The compacted internal topic that holds the coordination state of a cluster.
47pub const COORDINATION_STATE_TOPIC: &str = "__coordination_state";
48
49/// The maximum length of a [`Role`], in bytes.
50///
51/// A role becomes a Kafka `transactional.id`. The bound is the same bound Kafka
52/// puts on a topic name, so a role stays short enough to log and to compare.
53pub const MAX_ROLE_LEN: usize = 249;
54
55/// The maximum length of a [`MemberId`], in bytes.
56pub const MAX_MEMBER_ID_LEN: usize = 249;
57
58/// The version that every key and every value of the topic carries.
59const RECORD_VERSION: i16 = 0;
60
61/// The record part a decode failed on. Each names the [`RecordError::Format`]
62/// `part` field.
63const KEY_PART: &str = "key";
64const REGISTRATION_VALUE_PART: &str = "registration value";
65const LEASE_VALUE_PART: &str = "lease value";
66
67/// The field names that [`RecordError::Empty`] and [`RecordError::TooLong`]
68/// report.
69const ROLE_FIELD: &str = "role";
70const MEMBER_FIELD: &str = "member id";
71
72/// A failure to build or to decode a coordination state record.
73///
74/// [`RecordError::Format`] reports malformed bytes, and its `part` field names
75/// the record part that failed. The other variants report a value that a
76/// caller or a record gave, and each names the field it rejects.
77#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
78#[non_exhaustive]
79pub enum RecordError {
80    /// The bytes do not match the frozen layout of the record part.
81    #[error("malformed coordination state {part}: {message}")]
82    Format {
83        /// The record part the decoder failed on. It is `key`,
84        /// `registration value`, or `lease value`.
85        part: &'static str,
86        /// What the decoder found.
87        message: String,
88    },
89
90    /// A role or a member id is the empty string.
91    #[error("a coordination {field} must not be empty")]
92    Empty {
93        /// The field that is empty.
94        field: &'static str,
95    },
96
97    /// A role or a member id is longer than its bound.
98    #[error("a coordination {field} of {length} bytes is longer than the {bound}-byte maximum")]
99    TooLong {
100        /// The field that is too long.
101        field: &'static str,
102        /// The length the caller gave, in bytes.
103        length: usize,
104        /// The maximum length of the field, in bytes.
105        bound: usize,
106    },
107
108    /// A fencing token carries a negative producer id or a negative producer
109    /// epoch.
110    #[error("a fencing token must not be negative, got {producer_id}:{producer_epoch}")]
111    NegativeFencingToken {
112        /// The producer id the caller gave.
113        producer_id: i64,
114        /// The producer epoch the caller gave.
115        producer_epoch: i16,
116    },
117
118    /// A string does not have the `producer_id:producer_epoch` form of a
119    /// fencing token.
120    #[error("expected a fencing token of the form producer_id:producer_epoch, got {text:?}")]
121    FencingTokenForm {
122        /// The string the caller gave.
123        text: String,
124    },
125}
126
127/// The name of a coordination role, such as the name of an active controller.
128///
129/// A role becomes a Kafka `transactional.id`. The transaction coordinator mints
130/// one producer epoch for that id, so the role names the fenced group of
131/// members that compete for the same leadership.
132///
133/// [`Role::new`] is the only way to build one, and it rejects an empty name and
134/// a name longer than [`MAX_ROLE_LEN`]. A `Role` value is proof that the name
135/// is well formed, so this type does not implement `From<String>`.
136#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
137pub struct Role {
138    name: String,
139    /// The `i16` byte length that the frozen layout writes before the name.
140    wire_length: i16,
141}
142
143impl Role {
144    /// Parses a role name.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`RecordError::Empty`] when `role` is the empty string. Returns
149    /// [`RecordError::TooLong`] when `role` holds more than [`MAX_ROLE_LEN`]
150    /// bytes.
151    pub fn new(role: &str) -> Result<Self, RecordError> {
152        let wire_length = check_name(role, ROLE_FIELD, MAX_ROLE_LEN)?;
153        Ok(Self {
154            name: role.to_owned(),
155            wire_length,
156        })
157    }
158
159    /// The role name.
160    #[must_use]
161    pub fn as_str(&self) -> &str {
162        &self.name
163    }
164}
165
166impl fmt::Display for Role {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str(&self.name)
169    }
170}
171
172/// The identity of one member that competes for a role.
173///
174/// A member picks its own id and keeps it across a reconnect, so the succession
175/// order survives a short network failure.
176///
177/// [`MemberId::new`] is the only way to build one, and it rejects an empty id
178/// and an id longer than [`MAX_MEMBER_ID_LEN`]. A `MemberId` value is proof
179/// that the id is well formed, so this type does not implement `From<String>`.
180#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
181pub struct MemberId {
182    id: String,
183    /// The `i16` byte length that the frozen layout writes before the id.
184    wire_length: i16,
185}
186
187impl MemberId {
188    /// Parses a member id.
189    ///
190    /// # Errors
191    ///
192    /// Returns [`RecordError::Empty`] when `member` is the empty string.
193    /// Returns [`RecordError::TooLong`] when `member` holds more than
194    /// [`MAX_MEMBER_ID_LEN`] bytes.
195    pub fn new(member: &str) -> Result<Self, RecordError> {
196        let wire_length = check_name(member, MEMBER_FIELD, MAX_MEMBER_ID_LEN)?;
197        Ok(Self {
198            id: member.to_owned(),
199            wire_length,
200        })
201    }
202
203    /// The member id.
204    #[must_use]
205    pub fn as_str(&self) -> &str {
206        &self.id
207    }
208}
209
210impl fmt::Display for MemberId {
211    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212        formatter.write_str(&self.id)
213    }
214}
215
216/// Rejects an empty name and a name past `bound`, and returns the `i16` byte
217/// length of an accepted name.
218fn check_name(name: &str, field: &'static str, bound: usize) -> Result<i16, RecordError> {
219    if name.is_empty() {
220        return Err(RecordError::Empty { field });
221    }
222    let too_long = || RecordError::TooLong {
223        field,
224        length: name.len(),
225        bound,
226    };
227    if name.len() > bound {
228        return Err(too_long());
229    }
230    i16::try_from(name.len()).map_err(|_error| too_long())
231}
232
233/// The quorum-minted proof that one member holds a role.
234///
235/// The transaction coordinator mints the pair when a member calls
236/// `InitProducerId` for the `transactional.id` of the role. The quorum picks
237/// the values, the broker enforces them, and a write from an older pair fails
238/// with a fenced error. A holder passes the token to every guarded write.
239///
240/// The field order is load-bearing. The derived [`Ord`] compares `producer_id`
241/// first and `producer_epoch` second, so it is lexicographic. A producer epoch
242/// is an `i16` and it wraps. Kafka then allocates a new producer id and resets
243/// the epoch to zero. A comparison on the epoch alone would rank that fresh
244/// token below the stale one, and the old leader would keep the role. Never
245/// reorder these two fields, and never compare the epoch on its own.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
247pub struct FencingToken {
248    producer_id: i64,
249    producer_epoch: i16,
250}
251
252impl FencingToken {
253    /// Builds a fencing token from a minted producer id and producer epoch.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`RecordError::NegativeFencingToken`] when either value is
258    /// negative. Kafka writes `-1` for "no producer", and that pair never
259    /// proves leadership.
260    pub fn new(producer_id: i64, producer_epoch: i16) -> Result<Self, RecordError> {
261        if producer_id < 0 || producer_epoch < 0 {
262            return Err(RecordError::NegativeFencingToken {
263                producer_id,
264                producer_epoch,
265            });
266        }
267        Ok(Self {
268            producer_id,
269            producer_epoch,
270        })
271    }
272
273    /// The producer id the transaction coordinator minted.
274    #[must_use]
275    pub const fn producer_id(self) -> i64 {
276        self.producer_id
277    }
278
279    /// The producer epoch the transaction coordinator minted.
280    #[must_use]
281    pub const fn producer_epoch(self) -> i16 {
282        self.producer_epoch
283    }
284}
285
286impl fmt::Display for FencingToken {
287    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(formatter, "{}:{}", self.producer_id, self.producer_epoch)
289    }
290}
291
292impl FromStr for FencingToken {
293    type Err = RecordError;
294
295    fn from_str(serialized: &str) -> Result<Self, Self::Err> {
296        let form = || RecordError::FencingTokenForm {
297            text: serialized.to_owned(),
298        };
299        let (producer_id, producer_epoch) = serialized.split_once(':').ok_or_else(form)?;
300        if producer_epoch.contains(':') {
301            return Err(form());
302        }
303        Self::new(
304            producer_id.parse().map_err(|_error| form())?,
305            producer_epoch.parse().map_err(|_error| form())?,
306        )
307    }
308}
309
310/// Which record of [`COORDINATION_STATE_TOPIC`] a key names.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
312pub enum RecordKind {
313    /// One member of a role announces that it is available.
314    Registration,
315    /// One role names the member that holds it now.
316    Lease,
317}
318
319impl RecordKind {
320    /// The wire code a writer puts in the key.
321    #[must_use]
322    pub const fn code(self) -> i16 {
323        match self {
324            Self::Registration => 0,
325            Self::Lease => 1,
326        }
327    }
328
329    /// Maps a wire code onto a record kind.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`RecordError::Format`] when no record kind carries `code`.
334    pub fn from_code(code: i16) -> Result<Self, RecordError> {
335        match code {
336            0 => Ok(Self::Registration),
337            1 => Ok(Self::Lease),
338            other => Err(RecordError::Format {
339                part: KEY_PART,
340                message: format!("unknown coordination state record kind {other}"),
341            }),
342        }
343    }
344}
345
346/// The decoded key of a coordination state record.
347///
348/// `member` names one member for [`RecordKind::Registration`], and it is `None`
349/// for [`RecordKind::Lease`]. A lease belongs to the role and not to one
350/// member, so the lease key holds the empty string in that position.
351/// [`CoordinationKey::registration`] and [`CoordinationKey::lease`] build a key
352/// that keeps this rule.
353#[derive(Debug, Clone, PartialEq, Eq, Hash)]
354pub struct CoordinationKey {
355    /// The record the key names.
356    pub kind: RecordKind,
357    /// The role the record belongs to.
358    pub role: Role,
359    /// The member for a registration key, and `None` for a lease key.
360    pub member: Option<MemberId>,
361}
362
363impl CoordinationKey {
364    /// The key of the registration of one member of a role.
365    #[must_use]
366    pub fn registration(role: Role, member: MemberId) -> Self {
367        Self {
368            kind: RecordKind::Registration,
369            role,
370            member: Some(member),
371        }
372    }
373
374    /// The key of the lease of a role.
375    #[must_use]
376    pub fn lease(role: Role) -> Self {
377        Self {
378            kind: RecordKind::Lease,
379            role,
380            member: None,
381        }
382    }
383}
384
385/// One member of a role announces that it is available.
386///
387/// A member writes a registration when it joins, and it writes a tombstone on
388/// the same key when it leaves.
389#[derive(Debug, Clone, PartialEq, Eq, Hash)]
390pub struct Registration {
391    /// The member that registered.
392    pub member: MemberId,
393    /// The time the member registered, in milliseconds since the Unix epoch.
394    pub registered_at: i64,
395}
396
397/// The member that holds a role now, and the time its claim ends.
398///
399/// The `token` is the authority. The broker fences a write that carries an
400/// older token, and no clock takes part in that check. The `deadline` is an
401/// anti-flap device. A standby waits for the deadline to pass before it
402/// challenges the holder, so a short pause does not move the role.
403#[derive(Debug, Clone, PartialEq, Eq, Hash)]
404pub struct Lease {
405    /// The member that holds the role.
406    pub member: MemberId,
407    /// The quorum-minted proof of the claim of the holder.
408    pub token: FencingToken,
409    /// The time the holder took the lease, in milliseconds since the Unix
410    /// epoch.
411    pub granted_at: i64,
412    /// The time the lease expires, in milliseconds since the Unix epoch.
413    pub deadline: i64,
414}
415
416/// One decoded value of [`COORDINATION_STATE_TOPIC`].
417#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub enum CoordinationRecord {
419    /// The value of a registration record.
420    Registration(Registration),
421    /// The value of a lease record.
422    Lease(Lease),
423    /// A null value. It deregisters the member of a registration key, and it
424    /// clears the lease of a lease key.
425    Tombstone,
426}
427
428/// Encodes a key of [`COORDINATION_STATE_TOPIC`].
429///
430/// The function writes the empty member string for a lease key, whatever
431/// `key.member` holds, because the frozen layout gives a lease no member.
432#[must_use]
433pub fn encode_key(key: &CoordinationKey) -> Bytes {
434    let (member, member_length) = match key.kind {
435        RecordKind::Registration => key
436            .member
437            .as_ref()
438            .map_or(("", 0), |member| (member.as_str(), member.wire_length)),
439        RecordKind::Lease => ("", 0),
440    };
441    let role = key.role.as_str();
442    let mut buffer = BytesMut::with_capacity(8 + role.len() + member.len());
443    buffer.put_i16(RECORD_VERSION);
444    buffer.put_i16(key.kind.code());
445    put_string(&mut buffer, role, key.role.wire_length);
446    put_string(&mut buffer, member, member_length);
447    buffer.freeze()
448}
449
450/// Encodes a value of [`COORDINATION_STATE_TOPIC`].
451///
452/// The result is `None` for [`CoordinationRecord::Tombstone`], because a
453/// tombstone is a record with a null value.
454#[must_use]
455pub fn encode_value(record: &CoordinationRecord) -> Option<Bytes> {
456    match record {
457        CoordinationRecord::Registration(registration) => {
458            let member = &registration.member;
459            let mut buffer = BytesMut::with_capacity(12 + member.as_str().len());
460            buffer.put_i16(RECORD_VERSION);
461            put_string(&mut buffer, member.as_str(), member.wire_length);
462            buffer.put_i64(registration.registered_at);
463            Some(buffer.freeze())
464        }
465        CoordinationRecord::Lease(lease) => {
466            let member = &lease.member;
467            let mut buffer = BytesMut::with_capacity(30 + member.as_str().len());
468            buffer.put_i16(RECORD_VERSION);
469            put_string(&mut buffer, member.as_str(), member.wire_length);
470            buffer.put_i64(lease.token.producer_id());
471            buffer.put_i16(lease.token.producer_epoch());
472            buffer.put_i64(lease.granted_at);
473            buffer.put_i64(lease.deadline);
474            Some(buffer.freeze())
475        }
476        CoordinationRecord::Tombstone => None,
477    }
478}
479
480/// Decodes a key of [`COORDINATION_STATE_TOPIC`].
481///
482/// # Errors
483///
484/// Returns [`RecordError::Format`] when the buffer is truncated, when it holds
485/// trailing bytes, when the version is not `0`, when the kind is neither `0`
486/// nor `1`, when a string length is negative, when a string is not UTF-8, or
487/// when a lease key names a member. Returns [`RecordError::Empty`] or
488/// [`RecordError::TooLong`] when the role, or the member of a registration key,
489/// is empty or too long.
490pub fn decode_key(bytes: &[u8]) -> Result<CoordinationKey, RecordError> {
491    let mut reader = Reader::new(bytes, KEY_PART);
492    reader.version()?;
493    let kind = RecordKind::from_code(reader.i16()?)?;
494    let role = reader.string()?;
495    let member = reader.string()?;
496    reader.finish()?;
497    let member = match kind {
498        RecordKind::Registration => Some(MemberId::new(member)?),
499        RecordKind::Lease => {
500            if !member.is_empty() {
501                return Err(reader.malformed(format!(
502                    "a lease key carries the empty member string, got {member:?}"
503                )));
504            }
505            None
506        }
507    };
508    Ok(CoordinationKey {
509        kind,
510        role: Role::new(role)?,
511        member,
512    })
513}
514
515/// Decodes a value of [`COORDINATION_STATE_TOPIC`] under the kind of its key.
516///
517/// `bytes` is `None` for a record with a null value, and the result is then
518/// [`CoordinationRecord::Tombstone`] for either kind.
519///
520/// # Errors
521///
522/// Returns [`RecordError::Format`] when the buffer is truncated, when it holds
523/// trailing bytes, when the version is not `0`, when a string length is
524/// negative, or when a string is not UTF-8. Returns [`RecordError::Empty`] or
525/// [`RecordError::TooLong`] when the member is empty or too long. Returns
526/// [`RecordError::NegativeFencingToken`] when a lease value carries a negative
527/// producer id or producer epoch.
528pub fn decode_value(
529    kind: RecordKind,
530    bytes: Option<&[u8]>,
531) -> Result<CoordinationRecord, RecordError> {
532    let Some(bytes) = bytes else {
533        return Ok(CoordinationRecord::Tombstone);
534    };
535    match kind {
536        RecordKind::Registration => {
537            decode_registration(bytes).map(CoordinationRecord::Registration)
538        }
539        RecordKind::Lease => decode_lease(bytes).map(CoordinationRecord::Lease),
540    }
541}
542
543fn decode_registration(bytes: &[u8]) -> Result<Registration, RecordError> {
544    let mut reader = Reader::new(bytes, REGISTRATION_VALUE_PART);
545    reader.version()?;
546    let member = reader.string()?;
547    let registered_at = reader.i64()?;
548    reader.finish()?;
549    Ok(Registration {
550        member: MemberId::new(member)?,
551        registered_at,
552    })
553}
554
555fn decode_lease(bytes: &[u8]) -> Result<Lease, RecordError> {
556    let mut reader = Reader::new(bytes, LEASE_VALUE_PART);
557    reader.version()?;
558    let member = reader.string()?;
559    let producer_id = reader.i64()?;
560    let producer_epoch = reader.i16()?;
561    let granted_at = reader.i64()?;
562    let deadline = reader.i64()?;
563    reader.finish()?;
564    Ok(Lease {
565        member: MemberId::new(member)?,
566        token: FencingToken::new(producer_id, producer_epoch)?,
567        granted_at,
568        deadline,
569    })
570}
571
572/// Writes one string as its `i16` byte length `length` and then UTF-8 bytes.
573///
574/// [`Role::new`] and [`MemberId::new`] compute `length` when they accept a
575/// name.
576fn put_string(buffer: &mut BytesMut, value: &str, length: i16) {
577    buffer.put_i16(length);
578    buffer.put_slice(value.as_bytes());
579}
580
581/// Reads the big-endian layout of the coordination state topic.
582///
583/// Every integer is signed, and a string is an `i16` byte length and then UTF-8
584/// bytes. The reader never sizes an allocation from a length it read, and it
585/// never panics. It takes a subslice of the input and returns a
586/// [`RecordError`] for every malformed input.
587struct Reader<'a> {
588    data: &'a [u8],
589    part: &'static str,
590}
591
592impl<'a> Reader<'a> {
593    fn new(data: &'a [u8], part: &'static str) -> Self {
594        Self { data, part }
595    }
596
597    fn malformed(&self, message: String) -> RecordError {
598        RecordError::Format {
599            part: self.part,
600            message,
601        }
602    }
603
604    fn truncated(&self) -> RecordError {
605        self.malformed(format!("truncated coordination state {}", self.part))
606    }
607
608    /// Fails when bytes remain after the last field.
609    fn finish(&self) -> Result<(), RecordError> {
610        if self.data.is_empty() {
611            return Ok(());
612        }
613        Err(self.malformed(format!(
614            "trailing bytes in coordination state {}",
615            self.part
616        )))
617    }
618
619    fn take_array<const N: usize>(&mut self) -> Result<[u8; N], RecordError> {
620        let Some((head, rest)) = self.data.split_first_chunk::<N>() else {
621            return Err(self.truncated());
622        };
623        self.data = rest;
624        Ok(*head)
625    }
626
627    fn take(&mut self, count: usize) -> Result<&'a [u8], RecordError> {
628        let Some((head, rest)) = self.data.split_at_checked(count) else {
629            return Err(self.truncated());
630        };
631        self.data = rest;
632        Ok(head)
633    }
634
635    fn i16(&mut self) -> Result<i16, RecordError> {
636        Ok(i16::from_be_bytes(self.take_array()?))
637    }
638
639    fn i64(&mut self) -> Result<i64, RecordError> {
640        Ok(i64::from_be_bytes(self.take_array()?))
641    }
642
643    /// Reads one string. A negative length is malformed, and the `i16` length
644    /// bounds the read at 32767 bytes, so no wire value sizes an allocation.
645    fn string(&mut self) -> Result<&'a str, RecordError> {
646        let length = self.i16()?;
647        let length = usize::try_from(length).map_err(|_error| {
648            self.malformed(format!(
649                "negative string length {length} in coordination state {}",
650                self.part
651            ))
652        })?;
653        let bytes = self.take(length)?;
654        str::from_utf8(bytes).map_err(|_error| {
655            self.malformed(format!(
656                "non-UTF-8 string in coordination state {}",
657                self.part
658            ))
659        })
660    }
661
662    /// Reads the version and rejects every version but [`RECORD_VERSION`].
663    fn version(&mut self) -> Result<(), RecordError> {
664        let version = self.i16()?;
665        if version == RECORD_VERSION {
666            return Ok(());
667        }
668        Err(self.malformed(format!(
669            "unsupported coordination state {} version {version}",
670            self.part
671        )))
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use assert2::{assert, check};
678    use proptest::{collection, prelude::*};
679
680    use super::*;
681
682    /// The pattern of a valid role name and a valid member id in the property
683    /// tests.
684    const NAME: &str = "[a-zA-Z0-9._-]{1,64}";
685
686    fn role(name: &str) -> Role {
687        Role::new(name).unwrap()
688    }
689
690    fn member(name: &str) -> MemberId {
691        MemberId::new(name).unwrap()
692    }
693
694    /// A name renders as itself. The codec writes the name from `as_str`, so
695    /// a `Display` that dropped the text would leave a log line and an error
696    /// message naming nothing while the wire stayed correct.
697    #[test]
698    fn a_role_and_a_member_render_as_their_own_names() {
699        check!(role("controller").to_string() == "controller");
700        check!(member("node-1").to_string() == "node-1");
701        check!(
702            format!("{} holds {}", member("node-1"), role("controller"))
703                == "node-1 holds controller"
704        );
705    }
706
707    /// The frozen golden bytes of every record part.
708    ///
709    /// `krabka-streams-java` and `krabka-streams-go` assert the same arrays.
710    /// Copy an array into a port without a change. A change here is a change of
711    /// the wire format, and it needs the same change in the two ports.
712    mod golden {
713        use super::*;
714
715        const ROLE: &str = "controller";
716        const MEMBER: &str = "node-1";
717        const REGISTERED_AT: i64 = 1_700_000_000_000;
718        const GRANTED_AT: i64 = 1_700_000_000_000;
719        const DEADLINE: i64 = 1_700_000_030_000;
720        const PRODUCER_ID: i64 = 4242;
721        const PRODUCER_EPOCH: i16 = 7;
722
723        /// The key of the registration of member `node-1` for role
724        /// `controller`.
725        pub(super) const REGISTRATION_KEY: [u8; 24] = [
726            0x00, 0x00, // version i16 = 0
727            0x00, 0x00, // kind i16 = 0 (registration)
728            0x00, 0x0A, // role length i16 = 10
729            0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65, 0x72, // "controller"
730            0x00, 0x06, // member length i16 = 6
731            0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, // "node-1"
732        ];
733
734        /// The key of the lease of role `controller`. It carries no member.
735        pub(super) const LEASE_KEY: [u8; 18] = [
736            0x00, 0x00, // version i16 = 0
737            0x00, 0x01, // kind i16 = 1 (lease)
738            0x00, 0x0A, // role length i16 = 10
739            0x63, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65, 0x72, // "controller"
740            0x00, 0x00, // member length i16 = 0, the empty string
741        ];
742
743        /// The registration value of member `node-1` at 1700000000000.
744        pub(super) const REGISTRATION_VALUE: [u8; 18] = [
745            0x00, 0x00, // version i16 = 0
746            0x00, 0x06, // member length i16 = 6
747            0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, // "node-1"
748            0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0x68, 0x00, // registered_at = 1700000000000
749        ];
750
751        /// The lease value of member `node-1`, token `4242:7`, granted at
752        /// 1700000000000 and expiring at 1700000030000.
753        pub(super) const LEASE_VALUE: [u8; 36] = [
754            0x00, 0x00, // version i16 = 0
755            0x00, 0x06, // member length i16 = 6
756            0x6E, 0x6F, 0x64, 0x65, 0x2D, 0x31, // "node-1"
757            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x92, // producer_id i64 = 4242
758            0x00, 0x07, // producer_epoch i16 = 7
759            0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0x68, 0x00, // granted_at i64 = 1700000000000
760            0x00, 0x00, 0x01, 0x8B, 0xCF, 0xE5, 0xDD, 0x30, // deadline i64 = 1700000030000
761        ];
762
763        fn registration_key() -> CoordinationKey {
764            CoordinationKey::registration(role(ROLE), member(MEMBER))
765        }
766
767        fn lease_key() -> CoordinationKey {
768            CoordinationKey::lease(role(ROLE))
769        }
770
771        fn registration_value() -> CoordinationRecord {
772            CoordinationRecord::Registration(Registration {
773                member: member(MEMBER),
774                registered_at: REGISTERED_AT,
775            })
776        }
777
778        fn lease_value() -> CoordinationRecord {
779            CoordinationRecord::Lease(Lease {
780                member: member(MEMBER),
781                token: FencingToken::new(PRODUCER_ID, PRODUCER_EPOCH).unwrap(),
782                granted_at: GRANTED_AT,
783                deadline: DEADLINE,
784            })
785        }
786
787        #[test]
788        fn a_registration_key_encodes_to_the_frozen_bytes() {
789            check!(encode_key(&registration_key()).as_ref() == REGISTRATION_KEY.as_slice());
790            check!(decode_key(&REGISTRATION_KEY).unwrap() == registration_key());
791        }
792
793        #[test]
794        fn a_lease_key_encodes_to_the_frozen_bytes() {
795            check!(encode_key(&lease_key()).as_ref() == LEASE_KEY.as_slice());
796            check!(decode_key(&LEASE_KEY).unwrap() == lease_key());
797        }
798
799        #[test]
800        fn a_registration_value_encodes_to_the_frozen_bytes() {
801            let encoded = encode_value(&registration_value()).unwrap();
802            check!(encoded.as_ref() == REGISTRATION_VALUE.as_slice());
803            let decoded =
804                decode_value(RecordKind::Registration, Some(&REGISTRATION_VALUE)).unwrap();
805            check!(decoded == registration_value());
806        }
807
808        #[test]
809        fn a_lease_value_encodes_to_the_frozen_bytes() {
810            let encoded = encode_value(&lease_value()).unwrap();
811            check!(encoded.as_ref() == LEASE_VALUE.as_slice());
812            let decoded = decode_value(RecordKind::Lease, Some(&LEASE_VALUE)).unwrap();
813            check!(decoded == lease_value());
814        }
815    }
816
817    #[test]
818    fn the_topic_name_is_frozen() {
819        check!(COORDINATION_STATE_TOPIC == "__coordination_state");
820    }
821
822    #[test]
823    fn a_record_kind_maps_onto_its_wire_code() {
824        for kind in [RecordKind::Registration, RecordKind::Lease] {
825            check!(RecordKind::from_code(kind.code()).unwrap() == kind);
826        }
827        check!(RecordKind::Registration.code() == 0);
828        check!(RecordKind::Lease.code() == 1);
829        for code in [-1_i16, 2, 9, i16::MAX, i16::MIN] {
830            check!(RecordKind::from_code(code).is_err(), "kind {code}");
831        }
832    }
833
834    #[test]
835    fn a_tombstone_encodes_to_a_null_value() {
836        check!(encode_value(&CoordinationRecord::Tombstone).is_none());
837        for kind in [RecordKind::Registration, RecordKind::Lease] {
838            check!(decode_value(kind, None).unwrap() == CoordinationRecord::Tombstone);
839        }
840    }
841
842    #[test]
843    fn a_lease_key_ignores_a_member_the_caller_set() {
844        let key = CoordinationKey {
845            kind: RecordKind::Lease,
846            role: role("controller"),
847            member: Some(member("node-1")),
848        };
849        check!(
850            decode_key(&encode_key(&key)).unwrap() == CoordinationKey::lease(role("controller"))
851        );
852    }
853
854    #[test]
855    fn a_role_rejects_an_empty_name_and_a_long_name() {
856        assert!(let Err(RecordError::Empty { field }) = Role::new(""));
857        check!(field == "role");
858        let long = "r".repeat(MAX_ROLE_LEN + 1);
859        assert!(let Err(RecordError::TooLong { length, bound, .. }) = Role::new(&long));
860        check!(length == MAX_ROLE_LEN + 1);
861        check!(bound == MAX_ROLE_LEN);
862        check!(Role::new(&"r".repeat(MAX_ROLE_LEN)).is_ok());
863    }
864
865    #[test]
866    fn a_member_id_rejects_an_empty_name_and_a_long_name() {
867        assert!(let Err(RecordError::Empty { field }) = MemberId::new(""));
868        check!(field == "member id");
869        let long = "m".repeat(MAX_MEMBER_ID_LEN + 1);
870        check!(MemberId::new(&long).is_err());
871        check!(MemberId::new(&"m".repeat(MAX_MEMBER_ID_LEN)).is_ok());
872    }
873
874    #[test]
875    fn a_fencing_token_orders_on_the_producer_id_first() {
876        let fresh = FencingToken::new(5, 0).unwrap();
877        let wrapped = FencingToken::new(4, i16::MAX).unwrap();
878        check!(fresh > wrapped);
879        check!(FencingToken::new(4, 1).unwrap() > FencingToken::new(4, 0).unwrap());
880        check!(FencingToken::new(4, 0).unwrap() == FencingToken::new(4, 0).unwrap());
881    }
882
883    #[test]
884    fn a_fencing_token_rejects_a_negative_component() {
885        check!(FencingToken::new(-1, 0).is_err());
886        check!(FencingToken::new(0, -1).is_err());
887        check!(FencingToken::new(0, 0).is_ok());
888    }
889
890    #[test]
891    fn a_fencing_token_round_trips_through_its_string_form() {
892        let token = FencingToken::new(4242, 7).unwrap();
893        check!(token.to_string() == "4242:7");
894        check!("4242:7".parse::<FencingToken>().unwrap() == token);
895        for text in ["", "4242", "4242:7:1", "a:7", "4242:b", "-1:7", "4242:-1"] {
896            check!(text.parse::<FencingToken>().is_err(), "token {text:?}");
897        }
898    }
899
900    /// Malformed keys. Each returns an error, and none panics.
901    #[test]
902    fn a_malformed_key_returns_an_error() {
903        let mut trailing = golden::REGISTRATION_KEY.to_vec();
904        trailing.push(0x00);
905        let cases: [(&str, Vec<u8>); 10] = [
906            ("empty buffer", vec![]),
907            ("truncated version", vec![0x00]),
908            ("truncated kind", vec![0x00, 0x00, 0x00]),
909            (
910                "unknown version",
911                vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x01, b'a', 0x00, 0x01, b'm'],
912            ),
913            (
914                "unknown kind",
915                vec![0x00, 0x00, 0x00, 0x09, 0x00, 0x01, b'a', 0x00, 0x00],
916            ),
917            (
918                "negative role length",
919                vec![0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00],
920            ),
921            (
922                "role length past the buffer",
923                vec![0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, b'a'],
924            ),
925            (
926                "non-UTF-8 role",
927                vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x01, b'm'],
928            ),
929            (
930                "registration key with an empty member",
931                vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x01, b'a', 0x00, 0x00],
932            ),
933            ("trailing bytes", trailing),
934        ];
935        for (name, bytes) in cases {
936            check!(decode_key(&bytes).is_err(), "key case {name}");
937        }
938    }
939
940    #[test]
941    fn a_lease_key_that_names_a_member_returns_an_error() {
942        let bytes = vec![
943            0x00, 0x00, // version
944            0x00, 0x01, // kind = lease
945            0x00, 0x01, b'a', // role "a"
946            0x00, 0x01, b'm', // member "m", which a lease key must not carry
947        ];
948        assert!(let Err(RecordError::Format { part, .. }) = decode_key(&bytes));
949        check!(part == "key");
950    }
951
952    /// Malformed values of both kinds. Each returns an error, and none panics.
953    #[test]
954    fn a_malformed_value_returns_an_error() {
955        let mut trailing_registration = golden::REGISTRATION_VALUE.to_vec();
956        trailing_registration.push(0x00);
957        let mut trailing_lease = golden::LEASE_VALUE.to_vec();
958        trailing_lease.push(0x00);
959        let cases: [(&str, RecordKind, Vec<u8>); 9] = [
960            ("empty registration", RecordKind::Registration, vec![]),
961            (
962                "unknown registration version",
963                RecordKind::Registration,
964                vec![0x00, 0x01, 0x00, 0x01, b'm', 0, 0, 0, 0, 0, 0, 0, 1],
965            ),
966            (
967                "negative registration member length",
968                RecordKind::Registration,
969                vec![0x00, 0x00, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 1],
970            ),
971            (
972                "truncated registration timestamp",
973                RecordKind::Registration,
974                vec![0x00, 0x00, 0x00, 0x01, b'm', 0x00],
975            ),
976            (
977                "trailing bytes after a registration",
978                RecordKind::Registration,
979                trailing_registration,
980            ),
981            ("empty lease", RecordKind::Lease, vec![]),
982            (
983                "truncated lease deadline",
984                RecordKind::Lease,
985                vec![
986                    0x00, 0x00, 0x00, 0x01, b'm', 0, 0, 0, 0, 0, 0, 0, 1, 0x00, 0x00, 0, 0, 0, 0,
987                    0, 0, 0, 1,
988                ],
989            ),
990            (
991                "negative lease producer id",
992                RecordKind::Lease,
993                vec![
994                    0x00, 0x00, 0x00, 0x01, b'm', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
995                    0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2,
996                ],
997            ),
998            (
999                "trailing bytes after a lease",
1000                RecordKind::Lease,
1001                trailing_lease,
1002            ),
1003        ];
1004        for (name, kind, bytes) in cases {
1005            check!(
1006                decode_value(kind, Some(&bytes)).is_err(),
1007                "value case {name}"
1008            );
1009        }
1010    }
1011
1012    proptest! {
1013        #[test]
1014        fn a_registration_key_survives_a_round_trip(name in NAME, id in NAME) {
1015            let key = CoordinationKey::registration(role(&name), member(&id));
1016            let decoded = decode_key(&encode_key(&key)).unwrap();
1017            check!(decoded == key);
1018        }
1019
1020        #[test]
1021        fn a_lease_key_survives_a_round_trip(name in NAME) {
1022            let key = CoordinationKey::lease(role(&name));
1023            let decoded = decode_key(&encode_key(&key)).unwrap();
1024            check!(decoded == key);
1025        }
1026
1027        #[test]
1028        fn a_registration_value_survives_a_round_trip(
1029            id in NAME,
1030            registered_at in any::<i64>(),
1031        ) {
1032            let record = CoordinationRecord::Registration(Registration {
1033                member: member(&id),
1034                registered_at,
1035            });
1036            let encoded = encode_value(&record).unwrap();
1037            let decoded = decode_value(RecordKind::Registration, Some(&encoded)).unwrap();
1038            check!(decoded == record);
1039        }
1040
1041        #[test]
1042        fn a_lease_value_survives_a_round_trip(
1043            id in NAME,
1044            producer_id in 0_i64..=i64::MAX,
1045            producer_epoch in 0_i16..=i16::MAX,
1046            granted_at in any::<i64>(),
1047            deadline in any::<i64>(),
1048        ) {
1049            let record = CoordinationRecord::Lease(Lease {
1050                member: member(&id),
1051                token: FencingToken::new(producer_id, producer_epoch).unwrap(),
1052                granted_at,
1053                deadline,
1054            });
1055            let encoded = encode_value(&record).unwrap();
1056            let decoded = decode_value(RecordKind::Lease, Some(&encoded)).unwrap();
1057            check!(decoded == record);
1058        }
1059
1060        /// The decoders read bytes off the network, so no input may panic.
1061        #[test]
1062        fn a_decoder_never_panics_on_arbitrary_bytes(
1063            bytes in collection::vec(any::<u8>(), 0..64),
1064        ) {
1065            let _ = decode_key(&bytes);
1066            let _ = decode_value(RecordKind::Registration, Some(&bytes));
1067            let _ = decode_value(RecordKind::Lease, Some(&bytes));
1068        }
1069    }
1070}