1use std::collections::{BTreeMap, BTreeSet};
33
34use crate::{
35 error::StreamsClientError, membership::TopicPartition, runtime::iqv2::request::Position,
36};
37
38pub const BARRIER_STATE_TOPIC: &str = "__barrier_state";
40
41const RECORD_VERSION: i16 = 0;
43
44const CUT_KIND: i16 = 2;
47
48const KEY_PART: &str = "key";
51const VALUE_PART: &str = "cut value";
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub enum CutStatus {
61 Complete,
63 Partial,
65}
66
67impl CutStatus {
68 #[must_use]
70 pub fn code(self) -> i8 {
71 match self {
72 Self::Complete => 0,
73 Self::Partial => 1,
74 }
75 }
76
77 pub fn from_code(code: i8) -> Result<Self, StreamsClientError> {
84 match code {
85 0 => Ok(Self::Complete),
86 1 => Ok(Self::Partial),
87 other => Err(malformed(
88 VALUE_PART,
89 format!("unknown barrier cut status {other}"),
90 )),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct BarrierCut {
106 pub group: String,
108 pub epoch: i64,
111 pub triggered_at: i64,
113 pub completed_at: i64,
116 pub status: CutStatus,
118 pub offsets: Position,
121 pub missing: BTreeSet<TopicPartition>,
124}
125
126impl BarrierCut {
127 #[must_use]
131 pub fn complete(&self) -> bool {
132 self.status == CutStatus::Complete
133 }
134
135 #[must_use]
138 pub fn offset(&self, topic: &str, partition: i32) -> Option<i64> {
139 self.offsets.offset(topic, partition)
140 }
141
142 #[must_use]
147 pub fn reached(&self, topic: &str, partition: i32, position: i64) -> bool {
148 self.offset(topic, partition)
149 .is_none_or(|offset| position >= offset)
150 }
151
152 #[must_use]
154 pub fn partitions(&self) -> Vec<TopicPartition> {
155 self.offsets
156 .0
157 .iter()
158 .flat_map(|(topic, partitions)| {
159 partitions.keys().map(|partition| TopicPartition {
160 topic: topic.clone(),
161 partition: *partition,
162 })
163 })
164 .collect()
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub(crate) struct BarrierKey {
171 pub kind: i16,
172 pub group: String,
173 pub epoch: i64,
174}
175
176impl BarrierKey {
177 pub(crate) fn is_cut(&self) -> bool {
179 self.kind == CUT_KIND
180 }
181}
182
183fn malformed(part: &'static str, message: String) -> StreamsClientError {
184 StreamsClientError::BarrierFormat { part, message }
185}
186
187pub fn decode_barrier_cut(
197 key: &[u8],
198 value: Option<&[u8]>,
199) -> Result<Option<BarrierCut>, StreamsClientError> {
200 let key = decode_key(key)?;
201 if !key.is_cut() {
202 return Ok(None);
203 }
204 match value {
205 None => Ok(None),
206 Some(value) => decode_cut_value(&key, value).map(Some),
207 }
208}
209
210pub(crate) fn decode_key(data: &[u8]) -> Result<BarrierKey, StreamsClientError> {
212 let mut reader = Reader::new(data, KEY_PART);
213 let version = reader.i16()?;
214 if version != RECORD_VERSION {
215 return Err(malformed(
216 KEY_PART,
217 format!("unsupported barrier state key version {version}"),
218 ));
219 }
220 let kind = reader.i16()?;
221 let group = reader.string()?;
222 let epoch = reader.i64()?;
223 reader.finish()?;
224 Ok(BarrierKey { kind, group, epoch })
225}
226
227pub(crate) fn decode_cut_value(
230 key: &BarrierKey,
231 data: &[u8],
232) -> Result<BarrierCut, StreamsClientError> {
233 let mut reader = Reader::new(data, VALUE_PART);
234 let version = reader.i16()?;
235 if version != RECORD_VERSION {
236 return Err(malformed(
237 VALUE_PART,
238 format!("unsupported barrier state cut value version {version}"),
239 ));
240 }
241 let triggered_at = reader.i64()?;
242 let completed_at = reader.i64()?;
243 let status = CutStatus::from_code(reader.i8()?)?;
244 let offsets = reader.cut_offsets()?;
245 let missing = reader.missing_partitions()?;
246 reader.finish()?;
247 Ok(BarrierCut {
248 group: key.group.clone(),
249 epoch: key.epoch,
250 triggered_at,
251 completed_at,
252 status,
253 offsets,
254 missing,
255 })
256}
257
258struct Reader<'a> {
263 data: &'a [u8],
264 part: &'static str,
265}
266
267impl<'a> Reader<'a> {
268 fn new(data: &'a [u8], part: &'static str) -> Self {
269 Self { data, part }
270 }
271
272 fn truncated(&self) -> StreamsClientError {
273 malformed(self.part, format!("truncated barrier state {}", self.part))
274 }
275
276 fn finish(&self) -> Result<(), StreamsClientError> {
278 if self.data.is_empty() {
279 return Ok(());
280 }
281 Err(malformed(
282 self.part,
283 format!("trailing bytes in barrier state {}", self.part),
284 ))
285 }
286
287 fn take(&mut self, count: usize) -> Result<&'a [u8], StreamsClientError> {
288 let Some((head, rest)) = self.data.split_at_checked(count) else {
289 return Err(self.truncated());
290 };
291 self.data = rest;
292 Ok(head)
293 }
294
295 fn i8(&mut self) -> Result<i8, StreamsClientError> {
296 Ok(self.take(1)?[0].cast_signed())
297 }
298
299 fn i16(&mut self) -> Result<i16, StreamsClientError> {
300 let bytes = self.take(2)?;
301 Ok(i16::from_be_bytes(bytes.try_into().expect("two bytes")))
302 }
303
304 fn i32(&mut self) -> Result<i32, StreamsClientError> {
305 let bytes = self.take(4)?;
306 Ok(i32::from_be_bytes(bytes.try_into().expect("four bytes")))
307 }
308
309 fn i64(&mut self) -> Result<i64, StreamsClientError> {
310 let bytes = self.take(8)?;
311 Ok(i64::from_be_bytes(bytes.try_into().expect("eight bytes")))
312 }
313
314 fn string(&mut self) -> Result<String, StreamsClientError> {
315 let length = self.i16()?;
316 let length = usize::try_from(length).map_err(|_error| {
317 malformed(
318 self.part,
319 format!(
320 "negative string length {length} in barrier state {}",
321 self.part
322 ),
323 )
324 })?;
325 let bytes = self.take(length)?;
326 String::from_utf8(bytes.to_vec()).map_err(|_error| {
327 malformed(
328 self.part,
329 format!("non-UTF-8 string in barrier state {}", self.part),
330 )
331 })
332 }
333
334 fn count(&mut self) -> Result<i32, StreamsClientError> {
339 let count = self.i32()?;
340 if count < 0 {
341 return Err(malformed(
342 self.part,
343 format!(
344 "negative array length {count} in barrier state {}",
345 self.part
346 ),
347 ));
348 }
349 Ok(count)
350 }
351
352 fn cut_offsets(&mut self) -> Result<Position, StreamsClientError> {
353 let mut offsets: BTreeMap<String, BTreeMap<i32, i64>> = BTreeMap::new();
354 for _ in 0..self.count()? {
355 let topic = self.string()?;
356 let partitions = self.count()?;
357 let entry = offsets.entry(topic).or_default();
358 for _ in 0..partitions {
359 let partition = self.i32()?;
360 entry.insert(partition, self.i64()?);
361 }
362 }
363 Ok(Position(offsets))
364 }
365
366 fn missing_partitions(&mut self) -> Result<BTreeSet<TopicPartition>, StreamsClientError> {
367 let mut missing = BTreeSet::new();
368 for _ in 0..self.count()? {
369 let topic = self.string()?;
370 missing.insert(TopicPartition {
371 topic,
372 partition: self.i32()?,
373 });
374 }
375 Ok(missing)
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use assert2::{assert, check};
382
383 use super::*;
384 use crate::barrier::testing::{CutRecord, cut_key, cut_value};
385
386 #[test]
387 fn decodes_a_complete_cut_from_its_frozen_bytes() {
388 let record = CutRecord {
389 group: "transactions".to_owned(),
390 epoch: 7,
391 triggered_at: 100,
392 completed_at: 140,
393 status: CutStatus::Complete,
394 offsets: vec![("orders", 0, 12), ("orders", 1, 5), ("payments", 0, 31)],
395 missing: vec![],
396 };
397 let decoded = decode_barrier_cut(&cut_key(&record), Some(&cut_value(&record)))
398 .unwrap()
399 .unwrap();
400 check!(decoded == record.expected());
401 }
402
403 #[test]
404 fn decodes_a_partial_cut_with_its_missing_partitions() {
405 let record = CutRecord {
406 group: "transactions".to_owned(),
407 epoch: 8,
408 triggered_at: 200,
409 completed_at: 260,
410 status: CutStatus::Partial,
411 offsets: vec![("orders", 0, 40)],
412 missing: vec![("orders", 1)],
413 };
414 let decoded = decode_barrier_cut(&cut_key(&record), Some(&cut_value(&record)))
415 .unwrap()
416 .unwrap();
417 check!(decoded == record.expected());
418 check!(!decoded.complete());
419 }
420
421 #[test]
422 fn skips_other_kinds_and_tombstones() {
423 let record = CutRecord::simple("g", 3, &[("in", 0, 1)]);
424 for kind in [0i16, 1] {
426 let key = crate::barrier::testing::key_of_kind(kind, "g", 3);
427 check!(decode_barrier_cut(&key, Some(&cut_value(&record))).unwrap() == None);
428 }
429 check!(decode_barrier_cut(&cut_key(&record), None).unwrap() == None);
430 }
431
432 #[test]
433 fn cut_answers_offset_reached_and_partitions() {
434 let record = CutRecord::simple("g", 1, &[("in", 0, 10), ("in", 1, 4)]);
435 let cut = record.expected();
436 check!(cut.offset("in", 0) == Some(10));
437 check!(cut.offset("in", 2) == None);
438 check!(!cut.reached("in", 0, 9));
439 check!(cut.reached("in", 0, 10));
440 check!(cut.reached("other", 0, 0));
441 check!(
442 cut.partitions()
443 == vec![
444 TopicPartition {
445 topic: "in".to_owned(),
446 partition: 0
447 },
448 TopicPartition {
449 topic: "in".to_owned(),
450 partition: 1
451 },
452 ]
453 );
454 }
455
456 #[test]
457 fn rejects_malformed_records() {
458 let record = CutRecord::simple("g", 1, &[("in", 0, 10)]);
459 let key = cut_key(&record);
460 let value = cut_value(&record);
461
462 let mut bad_key_version = key.to_vec();
463 bad_key_version[1] = 9;
464 assert!(let Err(error) = decode_barrier_cut(&bad_key_version, Some(&value)));
465 check!(
466 error.to_string()
467 == "malformed barrier state key: unsupported barrier state key version 9"
468 );
469
470 assert!(let Err(error) = decode_barrier_cut(&key[..key.len() - 1], Some(&value)));
471 check!(error.to_string().contains("truncated barrier state key"));
472
473 let mut trailing_key = key.to_vec();
474 trailing_key.push(0);
475 assert!(let Err(error) = decode_barrier_cut(&trailing_key, Some(&value)));
476 check!(
477 error
478 .to_string()
479 .contains("trailing bytes in barrier state key")
480 );
481
482 let mut bad_status = value.to_vec();
483 bad_status[18] = 7;
484 assert!(let Err(error) = decode_barrier_cut(&key, Some(&bad_status)));
485 check!(error.to_string().contains("unknown barrier cut status 7"));
486
487 assert!(let Err(error) = decode_barrier_cut(&key, Some(&value[..value.len() - 2])));
488 check!(
489 error
490 .to_string()
491 .contains("truncated barrier state cut value")
492 );
493
494 let mut negative_count = value.to_vec();
495 negative_count[19] = 0xFF;
498 assert!(let Err(error) = decode_barrier_cut(&key, Some(&negative_count)));
499 check!(error.to_string().contains("negative array length"));
500 }
501
502 #[test]
503 fn status_codes_round_trip() {
504 for status in [CutStatus::Complete, CutStatus::Partial] {
505 check!(CutStatus::from_code(status.code()).unwrap() == status);
506 }
507 assert!(let Err(error) = CutStatus::from_code(2));
508 check!(error.to_string().contains("unknown barrier cut status 2"));
509 }
510}