Skip to main content

krabka_protocol/records/metadata/
control.rs

1//! `KRaft` control-record framing. Control records live in a batch with the
2//! control bit set; the record key is `version (i16) + type (i16)` and the
3//! value is the generated message body.
4
5use bytes::{Buf, BufMut, Bytes, BytesMut};
6
7use crate::{
8    Decode, Encode, ProtocolError,
9    owned::{
10        k_raft_version_record::KRaftVersionRecord, leader_change_message::LeaderChangeMessage,
11        snapshot_footer_record::SnapshotFooterRecord, snapshot_header_record::SnapshotHeaderRecord,
12        voters_record::VotersRecord,
13    },
14    records::{Attributes, Record, RecordBatch},
15};
16
17/// `KRaft` control record types (the i16 written after the i16 version in the
18/// key).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(i16)]
21pub enum ControlRecordType {
22    LeaderChange = 2,
23    SnapshotHeader = 3,
24    SnapshotFooter = 4,
25    KRaftVersion = 5,
26    Voters = 6,
27}
28
29impl TryFrom<i16> for ControlRecordType {
30    type Error = ProtocolError;
31
32    fn try_from(value: i16) -> Result<Self, Self::Error> {
33        match value {
34            2 => Ok(Self::LeaderChange),
35            3 => Ok(Self::SnapshotHeader),
36            4 => Ok(Self::SnapshotFooter),
37            5 => Ok(Self::KRaftVersion),
38            6 => Ok(Self::Voters),
39            _ => Err(ProtocolError::InvalidValue("unknown control record type")),
40        }
41    }
42}
43
44/// A decoded `KRaft` control record.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ControlRecord {
47    LeaderChange(LeaderChangeMessage),
48    SnapshotHeader(SnapshotHeaderRecord),
49    SnapshotFooter(SnapshotFooterRecord),
50    KRaftVersion(KRaftVersionRecord),
51    Voters(VotersRecord),
52}
53
54/// Control record key version (Kafka writes 0).
55const CONTROL_KEY_VERSION: i16 = 0;
56
57/// Build a control record key: `version(i16) + type(i16)`.
58#[must_use]
59pub fn control_record_key(ty: ControlRecordType) -> Bytes {
60    let mut key = BytesMut::with_capacity(4);
61    key.put_i16(CONTROL_KEY_VERSION);
62    key.put_i16(ty as i16);
63    key.freeze()
64}
65
66/// Decode a control-record key.
67///
68/// # Errors
69/// Returns a [`ProtocolError`] when the key is truncated, uses an unsupported
70/// key version, names an unknown record type, or contains trailing bytes.
71pub fn decode_control_record_key(key: &[u8]) -> Result<ControlRecordType, ProtocolError> {
72    let mut cur = key;
73    if cur.remaining() < 4 {
74        return Err(ProtocolError::UnexpectedEof {
75            needed: 4 - cur.remaining(),
76        });
77    }
78    let version = cur.get_i16();
79    if version != CONTROL_KEY_VERSION {
80        return Err(ProtocolError::InvalidValue(
81            "unsupported control record key version",
82        ));
83    }
84    let record_type = ControlRecordType::try_from(cur.get_i16())?;
85    if cur.has_remaining() {
86        return Err(ProtocolError::InvalidValue(
87            "trailing bytes after control record key",
88        ));
89    }
90    Ok(record_type)
91}
92
93impl ControlRecord {
94    /// The Kafka control-record type encoded in this record's key.
95    #[must_use]
96    pub fn record_type(&self) -> ControlRecordType {
97        match self {
98            Self::LeaderChange(_) => ControlRecordType::LeaderChange,
99            Self::SnapshotHeader(_) => ControlRecordType::SnapshotHeader,
100            Self::SnapshotFooter(_) => ControlRecordType::SnapshotFooter,
101            Self::KRaftVersion(_) => ControlRecordType::KRaftVersion,
102            Self::Voters(_) => ControlRecordType::Voters,
103        }
104    }
105
106    /// Encode this record's key and value exactly as Kafka writes them.
107    ///
108    /// # Errors
109    /// Returns a [`ProtocolError`] if the embedded record version is outside
110    /// the generated message's supported range.
111    pub fn encode_key_value(&self) -> Result<(Bytes, Bytes), ProtocolError> {
112        let mut value = BytesMut::new();
113        match self {
114            Self::LeaderChange(record) => record.encode(&mut value, record.version)?,
115            Self::SnapshotHeader(record) => record.encode(&mut value, record.version)?,
116            Self::SnapshotFooter(record) => record.encode(&mut value, record.version)?,
117            Self::KRaftVersion(record) => record.encode(&mut value, record.version)?,
118            Self::Voters(record) => record.encode(&mut value, record.version)?,
119        }
120        Ok((control_record_key(self.record_type()), value.freeze()))
121    }
122
123    /// Decode a Kafka control-record key and value.
124    ///
125    /// The first `i16` in every supported control-record value is its schema
126    /// version. The generated codec for that version must consume the complete
127    /// value.
128    ///
129    /// # Errors
130    /// Returns a [`ProtocolError`] for malformed keys, truncated values,
131    /// unsupported record versions, or trailing value bytes.
132    pub fn decode(key: &[u8], value: &[u8]) -> Result<Self, ProtocolError> {
133        let record_type = decode_control_record_key(key)?;
134        if value.len() < 2 {
135            return Err(ProtocolError::UnexpectedEof {
136                needed: 2 - value.len(),
137            });
138        }
139        let version = i16::from_be_bytes([value[0], value[1]]);
140        let mut cur = value;
141        let record = match record_type {
142            ControlRecordType::LeaderChange => {
143                Self::LeaderChange(LeaderChangeMessage::decode(&mut cur, version)?)
144            }
145            ControlRecordType::SnapshotHeader => {
146                Self::SnapshotHeader(SnapshotHeaderRecord::decode(&mut cur, version)?)
147            }
148            ControlRecordType::SnapshotFooter => {
149                Self::SnapshotFooter(SnapshotFooterRecord::decode(&mut cur, version)?)
150            }
151            ControlRecordType::KRaftVersion => {
152                Self::KRaftVersion(KRaftVersionRecord::decode(&mut cur, version)?)
153            }
154            ControlRecordType::Voters => Self::Voters(VotersRecord::decode(&mut cur, version)?),
155        };
156        if !cur.is_empty() {
157            return Err(ProtocolError::InvalidValue(
158                "trailing bytes after control record value",
159            ));
160        }
161        Ok(record)
162    }
163}
164
165/// Encode a typed control record as a single-record control batch.
166///
167/// # Errors
168/// Returns a [`ProtocolError`] if the record's embedded version is unsupported.
169pub fn encode_typed_control_batch(
170    base_offset: i64,
171    record: &ControlRecord,
172) -> Result<Bytes, ProtocolError> {
173    let (key, value) = record.encode_key_value()?;
174    Ok(encode_control_batch(base_offset, key, value))
175}
176
177/// Encode a single-record control batch at `base_offset` with the control bit
178/// set, returning the full v2 `RecordBatch` bytes (CRC computed by the encoder).
179///
180/// # Panics
181/// Panics only if the underlying record-batch encoder fails, which cannot happen
182/// for an uncompressed in-range single-record batch.
183#[must_use]
184pub fn encode_control_batch(base_offset: i64, key: Bytes, value: Bytes) -> Bytes {
185    let batch = RecordBatch {
186        base_offset,
187        attributes: Attributes::default().with_control(true),
188        records: vec![Record {
189            key: Some(key),
190            value: Some(value),
191            ..Default::default()
192        }],
193        ..Default::default()
194    };
195    let mut out = BytesMut::new();
196    batch
197        .encode(&mut out)
198        .expect("control batch encodes (no compression, in-range)");
199    out.freeze()
200}
201
202#[cfg(test)]
203mod tests {
204
205    use bytes::Buf;
206
207    use super::*;
208    use crate::{
209        UnknownTaggedFields,
210        owned::voters_record::{Endpoint, KRaftVersionFeature, Voter},
211        primitives::uuid::Uuid,
212    };
213
214    #[test]
215    fn control_key_is_version_then_type() {
216        let cases = [
217            (ControlRecordType::LeaderChange, 2),
218            (ControlRecordType::SnapshotHeader, 3),
219            (ControlRecordType::SnapshotFooter, 4),
220            (ControlRecordType::KRaftVersion, 5),
221            (ControlRecordType::Voters, 6),
222        ];
223        for (record_type, number) in cases {
224            let key = control_record_key(record_type);
225            let mut cur: &[u8] = &key;
226            assert2::assert!((cur.get_i16(), cur.get_i16()) == (0, number));
227            assert2::assert!(decode_control_record_key(&key).expect("decode key") == record_type);
228        }
229    }
230
231    #[test]
232    fn kip853_control_records_are_byte_exact_and_roundtrip() {
233        let version_record = ControlRecord::KRaftVersion(KRaftVersionRecord {
234            version: 0,
235            k_raft_version: 1,
236            unknown_tagged_fields: UnknownTaggedFields(vec![]),
237        });
238        let (version_key, version_value) = version_record.encode_key_value().expect("encode");
239        assert2::assert!(&version_key[..] == &[0, 0, 0, 5]);
240        assert2::assert!(&version_value[..] == &[0, 0, 0, 1, 0]);
241        assert2::assert!(
242            ControlRecord::decode(&version_key, &version_value).expect("decode") == version_record
243        );
244
245        let voters_record = ControlRecord::Voters(VotersRecord {
246            version: 0,
247            voters: vec![Voter {
248                voter_id: 7,
249                voter_directory_id: Uuid([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
250                endpoints: vec![Endpoint {
251                    name: "CONTROLLER".to_string(),
252                    host: "localhost".to_string(),
253                    port: 9_093,
254                    ..Default::default()
255                }],
256                k_raft_version_feature: KRaftVersionFeature {
257                    min_supported_version: 0,
258                    max_supported_version: 1,
259                    ..Default::default()
260                },
261                ..Default::default()
262            }],
263            ..Default::default()
264        });
265        let (voters_key, voters_value) = voters_record.encode_key_value().expect("encode");
266        let expected_value = hex::decode(concat!(
267            "0000",                             // record version
268            "02",                               // one voter (compact array)
269            "00000007",                         // voter id
270            "000102030405060708090a0b0c0d0e0f", // directory id
271            "02",                               // one endpoint (compact array)
272            "0b434f4e54524f4c4c4552",           // CONTROLLER
273            "0a6c6f63616c686f7374",             // localhost
274            "2385",                             // port 9093
275            "00",                               // endpoint tagged fields
276            "00000001",                         // supported versions 0..=1
277            "00",                               // feature tagged fields
278            "00",                               // voter tagged fields
279            "00",                               // record tagged fields
280        ))
281        .expect("valid hex");
282        assert2::assert!(&voters_key[..] == &[0, 0, 0, 6]);
283        assert2::assert!(&voters_value[..] == expected_value);
284        assert2::assert!(
285            ControlRecord::decode(&voters_key, &voters_value).expect("decode") == voters_record
286        );
287    }
288
289    #[test]
290    fn malformed_control_records_are_rejected() {
291        let bad_keys: &[&[u8]] = &[
292            &[0, 0, 0],       // truncated
293            &[0, 1, 0, 5],    // unsupported key version
294            &[0, 0, 0, 7],    // unknown type
295            &[0, 0, 0, 5, 0], // trailing byte
296        ];
297        for key in bad_keys {
298            assert2::assert!(decode_control_record_key(key).is_err());
299        }
300
301        let key = control_record_key(ControlRecordType::KRaftVersion);
302        assert2::assert!(ControlRecord::decode(&key, &[0]).is_err());
303        assert2::assert!(ControlRecord::decode(&key, &[0, 0, 0, 1, 0, 0]).is_err());
304    }
305
306    #[test]
307    fn control_batch_sets_control_bit() {
308        let key = control_record_key(ControlRecordType::LeaderChange);
309        let batch = encode_control_batch(42, key, bytes::Bytes::from_static(b"\x00\x00"));
310        let mut cur: &[u8] = &batch;
311        let decoded = RecordBatch::decode(&mut cur).expect("control batch decodes");
312        assert2::assert!(decoded.base_offset == 42);
313        assert2::assert!(cur.is_empty());
314        // magic byte at offset 16, attributes i16 at offset 21..23; control bit = 0x20.
315        let attrs = i16::from_be_bytes([batch[21], batch[22]]);
316        assert2::assert!(attrs & 0x20 != 0);
317    }
318}