1use {crate::error::LogError, krabka_protocol::records::RecordBatch};
9
10pub const BARRIER_CONTROL_TYPE: i16 = 1000;
28
29pub const ABORT_CONTROL_TYPE: i16 = 0;
31
32pub const COMMIT_CONTROL_TYPE: i16 = 1;
34
35pub(super) fn transaction_marker_flags(
41 batch: &RecordBatch,
42) -> Result<TransactionMarkerFlags, LogError> {
43 let marker = batch_control_marker_type(batch)?;
44 Ok(TransactionMarkerFlags {
45 is_abort: marker == Some(ABORT_CONTROL_TYPE),
46 is_commit: marker == Some(COMMIT_CONTROL_TYPE),
47 })
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub(super) struct TransactionMarkerFlags {
53 pub(super) is_abort: bool,
54 pub(super) is_commit: bool,
55}
56
57pub const CONTROL_RECORD_KEY_VERSION: i16 = 0;
63
64pub const END_TXN_MARKER_VALUE_VERSION: i16 = 0;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum ControlBatchKind {
74 Transaction,
79 Barrier,
83}
84
85pub fn control_batch_kind(batch: &RecordBatch) -> Option<ControlBatchKind> {
94 use krabka_verified::transaction::{LogBatchKind, log_batch_kind};
95 let key = batch
96 .records
97 .first()
98 .and_then(|record| record.key.as_deref())
99 .unwrap_or(&[]);
100 match log_batch_kind(batch.attributes.is_control_batch(), key) {
101 LogBatchKind::Data => None,
102 LogBatchKind::Barrier => Some(ControlBatchKind::Barrier),
103 LogBatchKind::Abort | LogBatchKind::Commit | LogBatchKind::OtherControl => {
104 Some(ControlBatchKind::Transaction)
105 }
106 }
107}
108
109const CONTROL_RECORD_KEY_BYTES: usize = 4;
112
113const END_TXN_MARKER_VALUE_BYTES: usize = 6;
116
117pub fn parse_control_marker_type(key: Option<&[u8]>) -> Result<i16, LogError> {
135 let key = key.unwrap_or_default();
136 let Some((&[v0, v1, t0, t1], _)) = key.split_first_chunk::<CONTROL_RECORD_KEY_BYTES>() else {
137 return Err(LogError::InvalidControlRecordSize {
138 record: "end control record key",
139 needed: CONTROL_RECORD_KEY_BYTES,
140 found: key.len(),
141 });
142 };
143 let version = i16::from_be_bytes([v0, v1]);
144 if version < CONTROL_RECORD_KEY_VERSION {
145 return Err(LogError::InvalidControlRecordVersion {
146 record: "control record",
147 version,
148 });
149 }
150 Ok(i16::from_be_bytes([t0, t1]))
151}
152
153pub fn parse_control_marker_coordinator_epoch(value: Option<&[u8]>) -> Result<i32, LogError> {
168 let value = value.unwrap_or_default();
169 let Some((&[v0, v1, e0, e1, e2, e3], _)) =
170 value.split_first_chunk::<END_TXN_MARKER_VALUE_BYTES>()
171 else {
172 return Err(LogError::InvalidControlRecordSize {
173 record: "end transaction marker value",
174 needed: END_TXN_MARKER_VALUE_BYTES,
175 found: value.len(),
176 });
177 };
178 let version = i16::from_be_bytes([v0, v1]);
179 if version < END_TXN_MARKER_VALUE_VERSION {
180 return Err(LogError::InvalidControlRecordVersion {
181 record: "end transaction marker",
182 version,
183 });
184 }
185 Ok(i32::from_be_bytes([e0, e1, e2, e3]))
186}
187
188pub fn batch_control_marker_type(batch: &RecordBatch) -> Result<Option<i16>, LogError> {
196 batch
197 .records
198 .first()
199 .map(|record| parse_control_marker_type(record.key.as_deref()))
200 .transpose()
201}
202
203pub fn batch_control_marker_coordinator_epoch(
210 batch: &RecordBatch,
211) -> Result<Option<i32>, LogError> {
212 batch
213 .records
214 .first()
215 .map(|record| parse_control_marker_coordinator_epoch(record.value.as_deref()))
216 .transpose()
217}
218
219pub fn check_control_record_versions(batch: &RecordBatch) -> Result<(), LogError> {
228 if !batch.attributes.is_control_batch() {
229 return Ok(());
230 }
231 if matches!(
232 batch_control_marker_type(batch)?,
233 Some(ABORT_CONTROL_TYPE | COMMIT_CONTROL_TYPE)
234 ) {
235 batch_control_marker_coordinator_epoch(batch)?;
236 }
237 Ok(())
238}
239
240#[cfg(test)]
241mod tests {
242 use {
243 super::*,
244 crate::{
245 compact::build_offset_map,
246 log::{
247 Log,
248 test_support::{
249 BarrierEpoch, BarrierMarkerSetup, KeyedRecord, PartitionState,
250 ProducerTransaction, TempLog, TransactionFields, abort_marker, barrier_marker,
251 barrier_marker_from_producer, commit_marker, compact_test_log, compaction_ctx,
252 control_key, control_value, install_stamps, keyed_batch, partition_state,
253 sample_batch, stamped_test_log, test_log, transactional_batch,
254 },
255 },
256 producer_snapshot::ProducerSnapshotEntry,
257 segment::Segment,
258 test_support::{aborted_txn, open_log},
259 txn_index::AbortedTxn,
260 },
261 assert2::check,
262 bytes::Bytes,
263 krabka_ids::{Offset, ProducerId},
264 krabka_protocol::records::Record,
265 krabka_units::prelude::mebibytes,
266 tempfile::tempdir,
267 };
268
269 #[derive(Debug, PartialEq, Eq)]
272 enum Parsed<T> {
273 Value(T),
274 TooShort { record: &'static str, found: usize },
275 BadVersion { record: &'static str, version: i16 },
276 OtherError(String),
277 }
278
279 impl<T> From<Result<T, LogError>> for Parsed<T> {
280 fn from(result: Result<T, LogError>) -> Self {
281 match result {
282 Ok(value) => Self::Value(value),
283 Err(LogError::InvalidControlRecordSize { record, found, .. }) => {
284 Self::TooShort { record, found }
285 }
286 Err(LogError::InvalidControlRecordVersion { record, version }) => {
287 Self::BadVersion { record, version }
288 }
289 Err(other) => Self::OtherError(other.to_string()),
290 }
291 }
292 }
293
294 #[derive(Debug, PartialEq, Eq)]
296 struct KeptRecord {
297 key: Bytes,
298 value: Bytes,
299 }
300
301 struct ParseCase<T> {
303 name: &'static str,
304 bytes: Option<&'static [u8]>,
305 expected: Parsed<T>,
306 }
307
308 struct AppendCase {
311 name: &'static str,
312 key: Option<Bytes>,
313 value: Option<Bytes>,
314 expected: Parsed<()>,
315 }
316
317 const KEY: &str = "end control record key";
318 const VALUE: &str = "end transaction marker value";
319
320 #[test]
324 fn end_marker_golden_bytes_decode() {
325 check!(
326 Parsed::from(parse_control_marker_type(Some(&[0x00, 0x00, 0x00, 0x01])))
327 == Parsed::Value(1)
328 );
329 check!(
330 Parsed::from(parse_control_marker_coordinator_epoch(Some(&[
331 0x00, 0x00, 0x00, 0x00, 0x00, 0x11
332 ]))) == Parsed::Value(17)
333 );
334 }
335
336 #[test]
340 fn a_control_key_parses_as_kafka_parses_it() {
341 let cases: [ParseCase<i16>; 12] = [
342 ParseCase {
343 name: "ABORT",
344 bytes: Some(&[0, 0, 0, 0]),
345 expected: Parsed::Value(0),
346 },
347 ParseCase {
348 name: "COMMIT",
349 bytes: Some(&[0, 0, 0, 1]),
350 expected: Parsed::Value(1),
351 },
352 ParseCase {
353 name: "barrier",
354 bytes: Some(&[0, 0, 0x03, 0xe8]),
355 expected: Parsed::Value(1000),
356 },
357 ParseCase {
358 name: "a longer key",
359 bytes: Some(&[0, 0, 0, 1, 9, 9]),
360 expected: Parsed::Value(1),
361 },
362 ParseCase {
363 name: "a later version",
364 bytes: Some(&[0, 1, 0, 1]),
365 expected: Parsed::Value(1),
366 },
367 ParseCase {
368 name: "the largest version",
369 bytes: Some(&[0x7f, 0xff, 0, 1]),
370 expected: Parsed::Value(1),
371 },
372 ParseCase {
373 name: "minus one",
374 bytes: Some(&[0xff, 0xff, 0, 1]),
375 expected: Parsed::BadVersion {
376 record: "control record",
377 version: -1,
378 },
379 },
380 ParseCase {
381 name: "the smallest version",
382 bytes: Some(&[0x80, 0x00, 0, 1]),
383 expected: Parsed::BadVersion {
384 record: "control record",
385 version: i16::MIN,
386 },
387 },
388 ParseCase {
389 name: "no key",
390 bytes: None,
391 expected: Parsed::TooShort {
392 record: KEY,
393 found: 0,
394 },
395 },
396 ParseCase {
397 name: "one byte",
398 bytes: Some(&[0]),
399 expected: Parsed::TooShort {
400 record: KEY,
401 found: 1,
402 },
403 },
404 ParseCase {
405 name: "two bytes",
406 bytes: Some(&[0, 0]),
407 expected: Parsed::TooShort {
408 record: KEY,
409 found: 2,
410 },
411 },
412 ParseCase {
413 name: "three bytes",
414 bytes: Some(&[0, 0, 0]),
415 expected: Parsed::TooShort {
416 record: KEY,
417 found: 3,
418 },
419 },
420 ];
421 for ParseCase {
422 name,
423 bytes,
424 expected,
425 } in cases
426 {
427 check!(
428 Parsed::from(parse_control_marker_type(bytes)) == expected,
429 "case {name}"
430 );
431 }
432 }
433
434 #[test]
438 fn an_end_marker_value_parses_as_kafka_parses_it() {
439 let cases: [ParseCase<i32>; 10] = [
440 ParseCase {
441 name: "current",
442 bytes: Some(&[0, 0, 0, 0, 0, 7]),
443 expected: Parsed::Value(7),
444 },
445 ParseCase {
446 name: "a longer value",
447 bytes: Some(&[0, 0, 0, 0, 0, 7, 9]),
448 expected: Parsed::Value(7),
449 },
450 ParseCase {
451 name: "a later version",
452 bytes: Some(&[0, 1, 0, 0, 0, 7, 9]),
453 expected: Parsed::Value(7),
454 },
455 ParseCase {
456 name: "the largest version",
457 bytes: Some(&[0x7f, 0xff, 0, 0, 0, 7]),
458 expected: Parsed::Value(7),
459 },
460 ParseCase {
461 name: "minus one",
462 bytes: Some(&[0xff, 0xff, 0, 0, 0, 7]),
463 expected: Parsed::BadVersion {
464 record: "end transaction marker",
465 version: -1,
466 },
467 },
468 ParseCase {
469 name: "the smallest version",
470 bytes: Some(&[0x80, 0x00, 0, 0, 0, 7]),
471 expected: Parsed::BadVersion {
472 record: "end transaction marker",
473 version: i16::MIN,
474 },
475 },
476 ParseCase {
477 name: "no value",
478 bytes: None,
479 expected: Parsed::TooShort {
480 record: VALUE,
481 found: 0,
482 },
483 },
484 ParseCase {
485 name: "version only",
486 bytes: Some(&[0, 0]),
487 expected: Parsed::TooShort {
488 record: VALUE,
489 found: 2,
490 },
491 },
492 ParseCase {
493 name: "four bytes",
494 bytes: Some(&[0, 0, 0, 0]),
495 expected: Parsed::TooShort {
496 record: VALUE,
497 found: 4,
498 },
499 },
500 ParseCase {
501 name: "five bytes",
502 bytes: Some(&[0, 0, 0, 0, 0]),
503 expected: Parsed::TooShort {
504 record: VALUE,
505 found: 5,
506 },
507 },
508 ];
509 for ParseCase {
510 name,
511 bytes,
512 expected,
513 } in cases
514 {
515 check!(
516 Parsed::from(parse_control_marker_coordinator_epoch(bytes)) == expected,
517 "case {name}"
518 );
519 }
520 }
521
522 fn append_open_barrier_transaction(log: &mut Log) {
523 let mut data = transactional_batch(1000, 2, &["a", "b"]);
524 data.base_sequence = 0;
525 log.append(&mut data).unwrap(); }
527
528 #[test]
532 fn an_end_marker_kafka_refuses_is_not_appended() {
533 let cases: [AppendCase; 4] = [
534 AppendCase {
535 name: "a negative key version",
536 key: Some(Bytes::from_static(&[0xff, 0xff, 0x00, 0x01])),
537 value: Some(control_value(17)),
538 expected: Parsed::BadVersion {
539 record: "control record",
540 version: -1,
541 },
542 },
543 AppendCase {
544 name: "a short key",
545 key: Some(Bytes::from_static(&[0x00, 0x00, 0x00])),
546 value: Some(control_value(17)),
547 expected: Parsed::TooShort {
548 record: KEY,
549 found: 3,
550 },
551 },
552 AppendCase {
553 name: "a negative value version",
554 key: Some(control_key(COMMIT_CONTROL_TYPE)),
555 value: Some(Bytes::from_static(&[0xff, 0xfe, 0, 0, 0, 17])),
556 expected: Parsed::BadVersion {
557 record: "end transaction marker",
558 version: -2,
559 },
560 },
561 AppendCase {
562 name: "no value",
563 key: Some(control_key(COMMIT_CONTROL_TYPE)),
564 value: None,
565 expected: Parsed::TooShort {
566 record: VALUE,
567 found: 0,
568 },
569 },
570 ];
571 for AppendCase {
572 name,
573 key,
574 value,
575 expected,
576 } in cases
577 {
578 let TempLog { dir: _dir, mut log } = test_log();
579 let mut data = transactional_batch(1000, 2, &["a"]);
580 data.base_sequence = 0;
581 log.append(&mut data).unwrap();
582 let before = partition_state(&log, &[1000]);
583
584 let mut marker = commit_marker(1000, 2);
585 marker.records[0].key = key;
586 marker.records[0].value = value;
587 let got = Parsed::from(log.append(&mut marker).map(|_| ()));
588
589 check!(got == expected, "case {name}");
590 check!(partition_state(&log, &[1000]) == before, "case {name}");
591 check!(log.log_end_offset() == Offset(1), "case {name}");
592 }
593 }
594
595 #[yare::parameterized(
604 no_producer_id = { "no producer id", barrier_marker().epoch(7).call() },
605 with_producer_id = {
606 "with a producer id",
607 barrier_marker_from_producer()
608 .marker(BarrierMarkerSetup {
609 epoch: BarrierEpoch(7),
610 ..Default::default()
611 })
612 .call(),
613 },
614 )]
615 fn a_barrier_marker_leaves_an_open_transaction_untouched(
616 name: &'static str,
617 barrier: RecordBatch,
618 ) {
619 let mut barrier = barrier;
620 let TempLog { dir: _dir, mut log } = stamped_test_log(40, 1);
621
622 append_open_barrier_transaction(&mut log);
623 let before = partition_state(&log, &[1000, -1]);
624
625 log.append(&mut barrier).unwrap(); check!(partition_state(&log, &[1000, -1]) == before, "case {name}");
628 check!(log.log_end_offset() == Offset(3), "case {name}");
631 check!(log.stamp_for_offset(Offset(2)) == None, "case {name}");
632 }
633
634 #[test]
635 fn barrier_marker_identity_fields_write_no_producer_tail() {
636 check!(Log::data_producer_tail(ProducerId(-1), -1, 0, Offset(2)).unwrap() == None);
639 check!(Log::data_producer_tail(ProducerId(1000), -1, 0, Offset(2)).unwrap() == None);
640 }
641
642 #[test]
645 fn a_barrier_marker_moves_the_lso_like_a_data_batch() {
646 for (name, open_transaction, want_lso, want_log_end) in [
647 ("no open transaction", false, Offset(1), Offset(1)),
648 ("open transaction", true, Offset(0), Offset(2)),
649 ] {
650 for (kind, is_barrier) in [("data batch", false), ("barrier marker", true)] {
651 let TempLog { dir: _dir, mut log } = test_log();
652 if open_transaction {
653 let mut data = transactional_batch(1000, 0, &["a"]);
654 data.base_sequence = 0;
655 log.append(&mut data).unwrap();
656 }
657 let mut batch = if is_barrier {
658 barrier_marker().call()
659 } else {
660 sample_batch(1)
661 };
662 log.append(&mut batch).unwrap();
663 check!(log.lso() == want_lso, "case {name} / {kind}");
664 check!(log.log_end_offset() == want_log_end, "case {name} / {kind}");
665 }
666 }
667 }
668
669 #[yare::parameterized(
672 commit = {
673 "commit",
674 commit_marker(1000, 2),
675 Vec::new(),
676 vec![Some(40), Some(40), None, None]
677 },
678 abort = {
679 "abort",
680 abort_marker(1000, 2),
681 vec![aborted_txn().call()],
682 vec![None, None, None, None]
683 },
684 )]
685 fn a_barrier_marker_does_not_disturb_a_following_end_marker(
686 name: &'static str,
687 marker: RecordBatch,
688 want_aborted: Vec<AbortedTxn>,
689 want_stamps: Vec<Option<u64>>,
690 ) {
691 let mut marker = marker;
692 let committed = ProducerSnapshotEntry {
693 producer_id: ProducerId(1000),
694 producer_epoch: 2,
695 last_sequence: 1,
696 last_offset: Offset(1),
697 offset_delta: 1,
698 timestamp: 0,
699 coordinator_epoch: 17,
700 current_txn_first_offset: None,
701 };
702 let TempLog { dir: _dir, mut log } = stamped_test_log(40, 1);
703
704 append_open_barrier_transaction(&mut log);
705 log.append(&mut barrier_marker().epoch(9).call()).unwrap(); check!(
707 log.lso() == Offset(0),
708 "case {name}: the barrier holds the LSO"
709 );
710 log.append(&mut marker).unwrap(); log.release_replicated_transactions(Offset(4));
713
714 check!(
715 partition_state(&log, &[1000])
716 == PartitionState {
717 lso: Offset(4),
718 transactions: vec![ProducerTransaction {
719 producer_id: 1000,
720 fields: TransactionFields {
721 coordinator_epoch: 17,
722 first_offset: None,
723 },
724 }],
725 aborted: want_aborted,
726 producers: vec![committed],
727 },
728 "case {name}"
729 );
730 let stamps: Vec<Option<u64>> = (0..4).map(|o| log.stamp_for_offset(Offset(o))).collect();
731 check!(stamps == want_stamps, "case {name}");
732 }
733
734 #[test]
737 fn barrier_markers_rebuild_identical_state_after_reopen() {
738 let dir = tempdir().unwrap();
739 let ids = [1000, 2000, 3000, -1];
740 let before = {
741 let mut log = open_log(dir.path());
742 log.append(&mut barrier_marker().call()).unwrap(); let mut committed = transactional_batch(1000, 2, &["a"]);
745 committed.base_sequence = 0;
746 log.append(&mut committed).unwrap(); log.append(&mut barrier_marker().epoch(2).call()).unwrap(); log.append(&mut commit_marker(1000, 2)).unwrap(); let mut rolled_back = transactional_batch(2000, 5, &["b", "c"]);
751 rolled_back.base_sequence = 0;
752 log.append(&mut rolled_back).unwrap(); log.append(&mut barrier_marker().epoch(3).call()).unwrap(); log.append(&mut abort_marker(2000, 5)).unwrap(); let mut still_open = transactional_batch(3000, 1, &["d"]);
757 still_open.base_sequence = 0;
758 log.append(&mut still_open).unwrap(); log.append(
763 &mut barrier_marker_from_producer()
764 .marker(BarrierMarkerSetup {
765 epoch: BarrierEpoch(4),
766 ..Default::default()
767 })
768 .producer(3000)
769 .producer_epoch(1)
770 .call(),
771 )
772 .unwrap(); partition_state(&log, &ids)
775 };
776 check!(before.lso == Offset(1));
779
780 let mut reopened = open_log(dir.path());
781 check!(partition_state(&reopened, &ids) == before);
782 check!(reopened.log_end_offset() == Offset(10));
783 check!(reopened.last_stable_offset(Offset(10)) == Offset(8));
787 }
788
789 #[test]
793 fn a_barrier_marker_keeps_stamp_ranges_across_a_restart() {
794 let dir = tempdir().unwrap();
795 {
796 let mut log = open_log(dir.path());
797 append_open_barrier_transaction(&mut log);
798 log.append(
801 &mut barrier_marker_from_producer()
802 .marker(BarrierMarkerSetup {
803 epoch: BarrierEpoch(5),
804 ..Default::default()
805 })
806 .call(),
807 )
808 .unwrap(); }
810
811 let mut reopened = open_log(dir.path());
812 install_stamps(&mut reopened, 40, 1);
813 check!(reopened.lso() == Offset(0));
814
815 reopened.append(&mut commit_marker(1000, 2)).unwrap(); check!(reopened.lso() == Offset(0));
818 check!(reopened.last_stable_offset(Offset(4)) == Offset(4));
819 let stamps: Vec<Option<u64>> = (0..4)
820 .map(|offset| reopened.stamp_for_offset(Offset(offset)))
821 .collect();
822 check!(stamps == vec![Some(40), Some(40), None, None]);
823 }
824
825 #[test]
828 fn compaction_keeps_barrier_markers_and_never_indexes_their_key() {
829 let TempLog { dir: _dir, mut log } = compact_test_log();
830 log.append(&mut keyed_batch(0, &[KeyedRecord {
831 offset_delta: 0,
832 key: b"k1",
833 value: b"v0",
834 }]))
835 .unwrap(); log.append(&mut barrier_marker().call()).unwrap(); log.append(&mut keyed_batch(0, &[KeyedRecord {
838 offset_delta: 0,
839 key: b"k1",
840 value: b"v1",
841 }]))
842 .unwrap(); log.append(&mut barrier_marker().epoch(2).call()).unwrap(); log.append(&mut keyed_batch(0, &[KeyedRecord {
845 offset_delta: 0,
846 key: b"tail",
847 value: b"t",
848 }]))
849 .unwrap(); let sealed: Vec<_> = log.segments.iter().map(Segment::sealed_input).collect();
854 let mut indexed: Vec<Bytes> = build_offset_map(&sealed, vec![], None)
855 .unwrap()
856 .into_keys()
857 .collect();
858 indexed.sort();
859 check!(indexed == vec![Bytes::from_static(b"k1")]);
860
861 log.compact(&compaction_ctx()).unwrap();
862
863 let out = log.read(Offset(0), mebibytes(1)).unwrap();
864 let kept_markers: Vec<Record> = out
866 .batches
867 .iter()
868 .filter(|batch| batch.attributes.is_control_batch())
869 .flat_map(|batch| batch.records.iter().cloned())
870 .collect();
871 let want_markers: Vec<Record> = [1, 2]
872 .into_iter()
873 .flat_map(|epoch| barrier_marker().epoch(epoch).call().records)
874 .collect();
875 check!(kept_markers == want_markers);
876 let kept_data: Vec<KeptRecord> = out
878 .batches
879 .iter()
880 .filter(|batch| !batch.attributes.is_control_batch())
881 .flat_map(|batch| batch.records.iter())
882 .map(|record| KeptRecord {
883 key: record.key.clone().unwrap(),
884 value: record.value.clone().unwrap(),
885 })
886 .collect();
887 check!(
888 kept_data
889 == vec![
890 KeptRecord {
891 key: Bytes::from_static(b"k1"),
892 value: Bytes::from_static(b"v1")
893 },
894 KeptRecord {
895 key: Bytes::from_static(b"tail"),
896 value: Bytes::from_static(b"t")
897 },
898 ]
899 );
900 }
901}