Skip to main content

krabka_protocol/krabka/barrier/
write_barrier_markers.rs

1//! `WriteBarrierMarkers`, api key 1014.
2//!
3//! The coordinator sends this request to a broker that leads a partition of the
4//! group. That broker appends one barrier marker to each named partition and
5//! returns the offset that each marker took.
6//!
7//! This is inter-broker traffic. A client never sends it. The fan-out follows
8//! the transaction-marker path, and it collects the returned offsets, which the
9//! transaction path does not need.
10
11use bytes::{Buf, BufMut};
12
13use crate::{
14    Decode, Encode, ProtocolError, ProtocolRequest, UnknownTaggedFields,
15    krabka::common::{
16        I16_LEN, I32_LEN, I64_LEN, array_len, check_version, decode_array, encode_array,
17        read_unknown_tagged_fields, unknown_tagged_fields_len, write_unknown_tagged_fields,
18    },
19    primitives::{
20        fixed::{get_i16, get_i32, get_i64, put_i16, put_i32, put_i64},
21        string_bytes::{compact_string_len, get_compact_string_owned, put_compact_string},
22    },
23};
24
25/// Api key of `WriteBarrierMarkers`.
26pub const API_KEY: i16 = 1014;
27/// Lowest version of `WriteBarrierMarkers` that this build speaks.
28pub const MIN_VERSION: i16 = 0;
29/// Highest version of `WriteBarrierMarkers` that this build speaks.
30pub const MAX_VERSION: i16 = 0;
31/// First version with flexible framing from KIP-482.
32pub const FLEXIBLE_MIN: i16 = 0;
33
34/// Reports whether `version` uses flexible framing from KIP-482.
35///
36/// The request header takes the same framing as the request body, so a caller
37/// that writes the header needs this answer.
38#[inline]
39#[must_use]
40pub fn is_flexible(version: i16) -> bool {
41    version >= FLEXIBLE_MIN
42}
43
44/// One partition for the target broker to mark, and its expected leader epoch.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct WritableBarrierPartition {
47    /// Partition index within the topic.
48    pub partition: i32,
49    /// Leader epoch that the coordinator read for this partition when it froze
50    /// the target set. The target broker compares this epoch against its own
51    /// leader epoch, and it rejects a mismatch with `FENCED_LEADER_EPOCH`. `-1`
52    /// means the coordinator had no epoch for this partition, and the target
53    /// broker does not fence on it.
54    pub expected_leader_epoch: i32,
55    /// Tagged fields that this build does not know.
56    pub unknown_tagged_fields: UnknownTaggedFields,
57}
58
59impl Default for WritableBarrierPartition {
60    fn default() -> Self {
61        Self {
62            partition: 0,
63            expected_leader_epoch: -1,
64            unknown_tagged_fields: UnknownTaggedFields::default(),
65        }
66    }
67}
68
69impl Encode for WritableBarrierPartition {
70    fn encode<B: BufMut>(&self, buf: &mut B, _version: i16) -> Result<(), ProtocolError> {
71        put_i32(buf, self.partition);
72        put_i32(buf, self.expected_leader_epoch);
73        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
74        Ok(())
75    }
76
77    fn encoded_len(&self, _version: i16) -> usize {
78        I32_LEN + I32_LEN + unknown_tagged_fields_len(&self.unknown_tagged_fields)
79    }
80}
81
82impl Decode<'_> for WritableBarrierPartition {
83    fn decode<B: Buf>(buf: &mut B, _version: i16) -> Result<Self, ProtocolError> {
84        Ok(Self {
85            partition: get_i32(buf)?,
86            expected_leader_epoch: get_i32(buf)?,
87            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
88        })
89    }
90}
91
92/// The partitions of one topic for the target broker to mark.
93#[derive(Debug, Clone, PartialEq, Eq, Default)]
94pub struct WritableBarrierTopic {
95    /// Topic name.
96    pub topic: String,
97    /// One entry for each partition that the target broker leads.
98    pub partitions: Vec<WritableBarrierPartition>,
99    /// Tagged fields that this build does not know.
100    pub unknown_tagged_fields: UnknownTaggedFields,
101}
102
103impl Encode for WritableBarrierTopic {
104    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
105        put_compact_string(buf, &self.topic);
106        encode_array(buf, &self.partitions, version)?;
107        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
108        Ok(())
109    }
110
111    fn encoded_len(&self, version: i16) -> usize {
112        compact_string_len(&self.topic)
113            + array_len(&self.partitions, version)
114            + unknown_tagged_fields_len(&self.unknown_tagged_fields)
115    }
116}
117
118impl Decode<'_> for WritableBarrierTopic {
119    fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
120        Ok(Self {
121            topic: get_compact_string_owned(buf)?,
122            partitions: decode_array(buf, version)?,
123            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
124        })
125    }
126}
127
128/// Asks a broker to append one barrier marker to each named partition.
129#[derive(Debug, Clone, PartialEq, Eq, Default)]
130pub struct WriteBarrierMarkersRequest {
131    /// Name of the barrier group that the epoch belongs to.
132    pub group: String,
133    /// Epoch that the coordinator assigned to this cut. The marker carries it.
134    pub epoch: i64,
135    /// Milliseconds since the Unix epoch, at the moment the coordinator started
136    /// the injection. The marker carries it.
137    pub triggered_at: i64,
138    /// Target partitions, grouped by topic.
139    pub topics: Vec<WritableBarrierTopic>,
140    /// Tagged fields that this build does not know.
141    pub unknown_tagged_fields: UnknownTaggedFields,
142}
143
144impl Encode for WriteBarrierMarkersRequest {
145    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
146        check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
147        put_compact_string(buf, &self.group);
148        put_i64(buf, self.epoch);
149        put_i64(buf, self.triggered_at);
150        encode_array(buf, &self.topics, version)?;
151        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
152        Ok(())
153    }
154
155    fn encoded_len(&self, version: i16) -> usize {
156        compact_string_len(&self.group)
157            + I64_LEN
158            + I64_LEN
159            + array_len(&self.topics, version)
160            + unknown_tagged_fields_len(&self.unknown_tagged_fields)
161    }
162}
163
164impl Decode<'_> for WriteBarrierMarkersRequest {
165    fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
166        check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
167        Ok(Self {
168            group: get_compact_string_owned(buf)?,
169            epoch: get_i64(buf)?,
170            triggered_at: get_i64(buf)?,
171            topics: decode_array(buf, version)?,
172            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
173        })
174    }
175}
176
177impl ProtocolRequest for WriteBarrierMarkersRequest {
178    const API_KEY: i16 = API_KEY;
179    const MIN_VERSION: i16 = MIN_VERSION;
180    const MAX_VERSION: i16 = MAX_VERSION;
181    const LATEST_STABLE_VERSION: i16 = MAX_VERSION;
182    const FLEXIBLE_MIN: i16 = FLEXIBLE_MIN;
183
184    type Response = WriteBarrierMarkersResponse;
185}
186
187/// The outcome for one partition that the target broker tried to mark.
188#[derive(Debug, Clone, PartialEq, Eq, Default)]
189pub struct WrittenBarrierPartition {
190    /// Partition index within the topic.
191    pub partition: i32,
192    /// Kafka error code. `0` means the append succeeded and `offset` holds the
193    /// position of the marker.
194    pub error_code: i16,
195    /// Offset that the barrier marker took. The coordinator writes this offset
196    /// into the cut for this partition.
197    pub offset: i64,
198    /// Tagged fields that this build does not know.
199    pub unknown_tagged_fields: UnknownTaggedFields,
200}
201
202impl Encode for WrittenBarrierPartition {
203    fn encode<B: BufMut>(&self, buf: &mut B, _version: i16) -> Result<(), ProtocolError> {
204        put_i32(buf, self.partition);
205        put_i16(buf, self.error_code);
206        put_i64(buf, self.offset);
207        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
208        Ok(())
209    }
210
211    fn encoded_len(&self, _version: i16) -> usize {
212        I32_LEN + I16_LEN + I64_LEN + unknown_tagged_fields_len(&self.unknown_tagged_fields)
213    }
214}
215
216impl Decode<'_> for WrittenBarrierPartition {
217    fn decode<B: Buf>(buf: &mut B, _version: i16) -> Result<Self, ProtocolError> {
218        Ok(Self {
219            partition: get_i32(buf)?,
220            error_code: get_i16(buf)?,
221            offset: get_i64(buf)?,
222            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
223        })
224    }
225}
226
227/// The outcomes for the partitions of one topic.
228#[derive(Debug, Clone, PartialEq, Eq, Default)]
229pub struct WrittenBarrierTopic {
230    /// Topic name.
231    pub topic: String,
232    /// One outcome for each partition that the request named for this topic.
233    pub partitions: Vec<WrittenBarrierPartition>,
234    /// Tagged fields that this build does not know.
235    pub unknown_tagged_fields: UnknownTaggedFields,
236}
237
238impl Encode for WrittenBarrierTopic {
239    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
240        put_compact_string(buf, &self.topic);
241        encode_array(buf, &self.partitions, version)?;
242        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
243        Ok(())
244    }
245
246    fn encoded_len(&self, version: i16) -> usize {
247        compact_string_len(&self.topic)
248            + array_len(&self.partitions, version)
249            + unknown_tagged_fields_len(&self.unknown_tagged_fields)
250    }
251}
252
253impl Decode<'_> for WrittenBarrierTopic {
254    fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
255        Ok(Self {
256            topic: get_compact_string_owned(buf)?,
257            partitions: decode_array(buf, version)?,
258            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
259        })
260    }
261}
262
263/// The offset that each barrier marker took.
264///
265/// This response carries no throttle time. It is inter-broker traffic, and the
266/// broker applies no client quota to it.
267#[derive(Debug, Clone, PartialEq, Eq, Default)]
268pub struct WriteBarrierMarkersResponse {
269    /// One entry for each topic that the request named, in request order.
270    pub topics: Vec<WrittenBarrierTopic>,
271    /// Tagged fields that this build does not know.
272    pub unknown_tagged_fields: UnknownTaggedFields,
273}
274
275impl Encode for WriteBarrierMarkersResponse {
276    fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
277        check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
278        encode_array(buf, &self.topics, version)?;
279        write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
280        Ok(())
281    }
282
283    fn encoded_len(&self, version: i16) -> usize {
284        array_len(&self.topics, version) + unknown_tagged_fields_len(&self.unknown_tagged_fields)
285    }
286}
287
288impl Decode<'_> for WriteBarrierMarkersResponse {
289    fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
290        check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
291        Ok(Self {
292            topics: decode_array(buf, version)?,
293            unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
294        })
295    }
296}
297
298#[cfg(test)]
299impl WriteBarrierMarkersRequest {
300    /// Builds a request with every field set, for the round-trip tests.
301    fn populated() -> Self {
302        Self {
303            group: "orders".to_string(),
304            epoch: 41,
305            triggered_at: 1_724_500_000_000,
306            topics: vec![
307                WritableBarrierTopic {
308                    topic: "orders".to_string(),
309                    partitions: vec![
310                        WritableBarrierPartition {
311                            partition: 0,
312                            expected_leader_epoch: 4,
313                            unknown_tagged_fields: UnknownTaggedFields::default(),
314                        },
315                        WritableBarrierPartition {
316                            partition: 3,
317                            expected_leader_epoch: -1,
318                            unknown_tagged_fields:
319                                crate::krabka::test_support::sample_tagged_fields(),
320                        },
321                        WritableBarrierPartition {
322                            partition: 7,
323                            expected_leader_epoch: 17,
324                            unknown_tagged_fields: UnknownTaggedFields::default(),
325                        },
326                    ],
327                    unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
328                },
329                WritableBarrierTopic {
330                    topic: "payments".to_string(),
331                    partitions: Vec::new(),
332                    unknown_tagged_fields: UnknownTaggedFields::default(),
333                },
334            ],
335            unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
336        }
337    }
338}
339
340#[cfg(test)]
341impl WriteBarrierMarkersResponse {
342    /// Builds a response with every field set, for the round-trip tests.
343    fn populated() -> Self {
344        Self {
345            topics: vec![WrittenBarrierTopic {
346                topic: "orders".to_string(),
347                partitions: vec![
348                    WrittenBarrierPartition {
349                        partition: 0,
350                        error_code: 0,
351                        offset: 9_412,
352                        unknown_tagged_fields: UnknownTaggedFields::default(),
353                    },
354                    WrittenBarrierPartition {
355                        partition: 3,
356                        error_code: 6,
357                        offset: -1,
358                        unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
359                    },
360                ],
361                unknown_tagged_fields: UnknownTaggedFields::default(),
362            }],
363            unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use assert2::assert;
371
372    use super::*;
373    use crate::krabka::test_support::{reject_truncated, roundtrip, unsupported_versions};
374
375    #[test]
376    fn default_roundtrips_all_versions() {
377        for version in MIN_VERSION..=MAX_VERSION {
378            roundtrip(&WriteBarrierMarkersRequest::default(), version);
379            roundtrip(&WriteBarrierMarkersResponse::default(), version);
380        }
381    }
382
383    #[test]
384    fn populated_roundtrips_all_versions() {
385        for version in MIN_VERSION..=MAX_VERSION {
386            roundtrip(&WriteBarrierMarkersRequest::populated(), version);
387            roundtrip(&WriteBarrierMarkersResponse::populated(), version);
388        }
389    }
390
391    #[test]
392    fn no_expected_leader_epoch_is_the_partition_default() {
393        assert!(
394            WritableBarrierPartition::default()
395                == WritableBarrierPartition {
396                    partition: 0,
397                    expected_leader_epoch: -1,
398                    unknown_tagged_fields: UnknownTaggedFields::default(),
399                }
400        );
401    }
402
403    #[test]
404    fn truncated_input_is_rejected() {
405        reject_truncated(&WriteBarrierMarkersRequest::populated(), MIN_VERSION);
406        reject_truncated(&WriteBarrierMarkersResponse::populated(), MIN_VERSION);
407    }
408
409    #[test]
410    fn rejects_unsupported_versions() {
411        for version in unsupported_versions(MIN_VERSION, MAX_VERSION) {
412            let request = WriteBarrierMarkersRequest::populated();
413            assert!(matches!(
414                request.encode(&mut Vec::<u8>::new(), version),
415                Err(ProtocolError::UnsupportedVersion {
416                    api_key: API_KEY,
417                    ..
418                })
419            ));
420        }
421    }
422}