1use {
10 super::{
11 Log,
12 control::{
13 TransactionMarkerFlags, batch_control_marker_coordinator_epoch,
14 transaction_marker_flags,
15 },
16 },
17 crate::{error::LogError, stamp_index::OffsetRange, txn_index::AbortedTxn},
18 krabka_ids::{Offset, ProducerId},
19 krabka_protocol::records::RecordBatch,
20};
21
22impl Log {
23 pub(super) fn refresh_lso(&mut self) -> Result<(), LogError> {
31 let log_end = self.log_end_offset();
32 let (first, last) = self.pending.bounds().unwrap_or((log_end, log_end));
36 let completed = self
37 .unreplicated
38 .first_key_value()
39 .map_or(log_end, |(start, _)| *start);
40 let starts = [first.0, last.0, completed.0];
41 self.lso = krabka_verified::first_unstable_offset(&starts, log_end.0)
42 .map(Offset)
43 .ok_or_else(|| {
44 LogError::Corrupt(format!(
45 "pending transaction starts beyond log end {log_end}"
46 ))
47 })?;
48 Ok(())
49 }
50
51 pub fn last_stable_offset(&mut self, high_watermark: Offset) -> Offset {
64 self.release_replicated_transactions(high_watermark);
65 self.lso.min(high_watermark)
66 }
67
68 pub fn release_replicated_transactions(&mut self, high_watermark: Offset) {
75 let before = self.unreplicated.len();
76 self.unreplicated
77 .retain(|_, marker_offset| *marker_offset >= high_watermark);
78 if self.unreplicated.len() != before
79 && let Err(error) = self.refresh_lso()
80 {
81 tracing::warn!(%error, "last stable offset refresh failed");
84 }
85 }
86
87 pub(super) fn apply_transaction_marker(
96 &mut self,
97 batch: &RecordBatch,
98 producer_id: ProducerId,
99 last_offset: Offset,
100 transaction_stamp: Option<u64>,
101 ) -> Result<(), LogError> {
102 let TransactionMarkerFlags {
104 is_abort,
105 is_commit,
106 } = transaction_marker_flags(batch)?;
107 let closes = krabka_verified::transaction_marker_closes(
108 is_abort,
109 is_commit,
110 self.pending.contains_key(producer_id),
111 );
112 if (is_abort || is_commit)
113 && producer_id.get() >= 0
114 && let Some(epoch) = batch_control_marker_coordinator_epoch(batch)?
115 {
116 self.coordinator_epochs.insert(producer_id, epoch);
117 }
118 if closes && is_abort {
119 let pending_start = self.pending.get(producer_id).map(|offset| offset.0);
120 let Some((start, last)) = krabka_verified::aborted_transaction_interval(
121 pending_start,
122 last_offset.0,
123 producer_id.get(),
124 ) else {
125 return Err(LogError::Corrupt(format!(
126 "invalid aborted transaction interval for producer {producer_id}"
127 )));
128 };
129 let other_starts: Vec<i64> = self
136 .pending
137 .iter()
138 .filter(|(other_pid, _)| **other_pid != producer_id)
139 .map(|(_, offset)| offset.0)
140 .chain(self.unreplicated.keys().next().map(|offset| offset.0))
141 .collect();
142 let last_stable_offset = krabka_verified::first_unstable_offset(
143 &other_starts,
144 last_offset.0 + 1,
145 )
146 .map(Offset)
147 .ok_or_else(|| {
148 LogError::Corrupt(format!(
149 "pending transaction starts beyond aborted marker offset for producer {producer_id}"
150 ))
151 })?;
152 self.active_txn_index.append(AbortedTxn {
153 start_offset: Offset(start),
154 last_offset: Offset(last),
155 producer_id,
156 last_stable_offset,
157 })?;
158 }
159 let stamp_ranges = self
160 .pending_stamp_ranges
161 .get(&producer_id)
162 .cloned()
163 .unwrap_or_default();
164 if closes && is_commit && !stamp_ranges.is_empty() {
165 let stamp = transaction_stamp
166 .or_else(|| self.stamp_source.as_ref().map(|source| source.next_stamp()));
167 if let Some(stamp) = stamp {
168 for OffsetRange { first, last } in stamp_ranges {
169 self.record_stamp_value(first, last, stamp)?;
170 }
171 }
172 }
173 if closes {
177 if let Some(first_offset) = self.pending.remove(producer_id) {
178 self.unreplicated.insert(first_offset, last_offset);
179 }
180 self.pending_stamp_ranges.remove(&producer_id);
181 }
182 Ok(())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use {
189 super::*,
190 crate::{
191 log::test_support::{
192 EncodedVerbatim, TempLog, TransactionFields, abort_marker, append_transaction,
193 commit_marker, install_stamps, sample_batch, test_batch_at, test_log,
194 tiny_segments, transaction_entries, transaction_fields, transactional_batch,
195 verbatim_from,
196 },
197 name,
198 segment::Segment,
199 test_support::{aborted_txn, open_log},
200 },
201 bytes::Bytes,
202 krabka_ids::LeaderEpoch,
203 std::fs,
204 tempfile::tempdir,
205 };
206
207 #[test]
210 fn transactional_batch_holds_lso() {
211 let TempLog { dir: _dir, mut log } = test_log();
212 let mut b0 = sample_batch(1);
214 log.append(&mut b0).unwrap();
215 assert2::assert!(log.lso() == log.log_end_offset());
216
217 let mut b1 = transactional_batch(1000, 0, &["a", "b"]); let old_lso = log.lso();
220 log.append(&mut b1).unwrap();
221 assert2::assert!(log.lso() == old_lso);
222
223 let mut commit = commit_marker(1000, 0);
225 log.append(&mut commit).unwrap();
226 let log_end = log.log_end_offset();
227 assert2::assert!(log.last_stable_offset(log_end) == log_end);
228 }
229
230 #[test]
231 fn negative_producer_ids_never_create_transaction_state() {
232 let dir = tempdir().unwrap();
233 {
234 let mut log = open_log(dir.path());
235 install_stamps(&mut log, 40, 1);
236
237 let mut owned = sample_batch(1);
238 owned.producer_id = -2;
239 owned.producer_epoch = 0;
240 owned.base_sequence = 0;
241 owned.attributes = owned.attributes.with_transactional(true);
242 log.append(&mut owned).unwrap();
243 assert2::assert!(log.lso() == Offset(1));
244 assert2::assert!(log.stamp_for_offset(Offset(0)) == Some(40));
245
246 let mut marker = commit_marker(-2, 0);
247 log.append(&mut marker).unwrap();
248 assert2::assert!(
249 transaction_fields(&log, ProducerId(-2))
250 == TransactionFields {
251 coordinator_epoch: -1,
252 first_offset: None,
253 }
254 );
255
256 let mut verbatim = test_batch_at(0);
257 verbatim.producer_id = -3;
258 verbatim.producer_epoch = 0;
259 verbatim.base_sequence = 0;
260 verbatim.attributes = verbatim.attributes.with_transactional(true);
261 let EncodedVerbatim { batch, .. } = verbatim_from(&verbatim, LeaderEpoch(0));
262 log.append_verbatim(&batch).unwrap();
263 assert2::assert!(log.lso() == Offset(3));
264 assert2::assert!(log.stamp_for_offset(Offset(2)) == Some(41));
265 assert2::assert!(log.producer_state_snapshot().is_empty());
266 }
267
268 let reopened = open_log(dir.path());
269 assert2::assert!(reopened.lso() == reopened.log_end_offset());
270 assert2::assert!(
271 transaction_fields(&reopened, ProducerId(-2))
272 == TransactionFields {
273 coordinator_epoch: -1,
274 first_offset: None,
275 }
276 );
277 assert2::assert!(
278 transaction_fields(&reopened, ProducerId(-3))
279 == TransactionFields {
280 coordinator_epoch: -1,
281 first_offset: None,
282 }
283 );
284 assert2::assert!(reopened.producer_state_snapshot().is_empty());
285 }
286
287 #[yare::parameterized(
288 single_offset = { 0, 3, 4 },
289 two_offset_marker = { 1, 4, 5 },
290 )]
291 fn abort_marker_records_its_base_plus_delta(
292 last_offset_delta: i32,
293 expected_last_offset: i64,
294 expected_lso: i64,
295 ) {
296 let TempLog { dir, mut log } = test_log();
297 append_transaction(&mut log, 1000, 0, &["a", "b", "c"]);
298
299 let mut a = abort_marker(1000, 0);
300 a.last_offset_delta = last_offset_delta;
301 log.append(&mut a).unwrap();
302
303 let entries = transaction_entries(dir.path(), "00000000000000000000.txnindex");
304 assert2::assert!(
306 entries
307 == [AbortedTxn {
308 start_offset: Offset(0),
309 last_offset: Offset(expected_last_offset),
310 producer_id: ProducerId(1000),
311 last_stable_offset: Offset(expected_lso),
312 }]
313 );
314 }
315
316 #[test]
324 fn abort_marker_lso_is_held_by_an_earlier_unreplicated_transaction() {
325 let TempLog { dir, mut log } = test_log();
326
327 let mut t1 = transactional_batch(1000, 0, &["a", "b"]);
331 log.append(&mut t1).unwrap();
332 log.append(&mut commit_marker(1000, 0)).unwrap();
333
334 let mut t2 = transactional_batch(2000, 0, &["c"]);
336 log.append(&mut t2).unwrap();
337 log.append(&mut abort_marker(2000, 0)).unwrap();
338
339 let entries = transaction_entries(dir.path(), "00000000000000000000.txnindex");
340 assert2::assert!(
344 entries
345 == [aborted_txn()
346 .producer(2000)
347 .bounds(Offset(3)..=Offset(4))
348 .stable(0)
349 .call()]
350 );
351 }
352
353 #[test]
354 fn aborted_transaction_uses_cached_marker_segment_beyond_range_end() {
355 let TempLog { dir, mut log } = test_log();
356 let mut transaction = transactional_batch(1000, 0, &["a", "b", "c"]);
357 log.append(&mut transaction).unwrap();
358
359 log.set_config(tiny_segments());
360 let (marker_base, _) = log.append(&mut abort_marker(1000, 0)).unwrap();
361 log.append(&mut sample_batch(1)).unwrap();
362 fs::remove_file(name::txnindex_path(dir.path(), marker_base.0)).unwrap();
363
364 assert2::assert!(log.aborted_in_range(Offset(0), marker_base) == [aborted_txn().call()]);
365 }
366
367 #[test]
372 fn non_txn_batch_with_valid_pid_advances_lso() {
373 let TempLog { dir: _dir, mut log } = test_log();
374 let mut b = sample_batch(2);
377 b.producer_id = 55;
378 b.producer_epoch = 0;
379 assert2::assert!(!b.attributes.is_transactional());
380 log.append(&mut b).unwrap();
381 assert2::assert!(log.lso() == Offset(2));
383 assert2::assert!(log.log_end_offset() == Offset(2));
384 }
385
386 #[test]
387 fn lso_held_by_remaining_producer_after_partial_commit() {
388 let TempLog { dir: _dir, mut log } = test_log();
389
390 let mut t1 = transactional_batch(1000, 0, &["a", "b"]);
392 log.append(&mut t1).unwrap();
393 let mut t2 = transactional_batch(2000, 0, &["c"]);
394 log.append(&mut t2).unwrap();
395 let lso_after_open = log.lso();
396
397 let mut c1 = commit_marker(1000, 0);
399 log.append(&mut c1).unwrap();
400 let log_end = log.log_end_offset();
401 assert2::assert!(log.last_stable_offset(log_end) == Offset(2));
402 assert2::assert!(log.lso() > lso_after_open);
403
404 let mut c2 = commit_marker(2000, 0);
406 log.append(&mut c2).unwrap();
407 let log_end = log.log_end_offset();
408 assert2::assert!(log.last_stable_offset(log_end) == log_end);
409 }
410
411 #[yare::parameterized(
412 malformed = { "malformed", true },
413 different_producer = { "different producer", false },
414 )]
415 fn only_a_valid_marker_for_the_pending_producer_closes_it(case: &str, malformed: bool) {
416 let dir = tempdir().unwrap();
417 {
418 let mut log = open_log(dir.path());
419 append_transaction(&mut log, 1000, 0, &["a", "b"]);
420 let held = log.lso();
421 let mut marker = if malformed {
422 let mut marker = commit_marker(1000, 0);
423 marker.records[0].key = Some(Bytes::from_static(&[0, 0, 0]));
424 marker
425 } else {
426 commit_marker(2000, 0)
427 };
428 let refused = log.append(&mut marker).err().map(|e| e.to_string());
432 let want = malformed.then(|| {
433 "Invalid value size found for end control record key. Must have at least 4 \
434 bytes, but found only 3"
435 .to_owned()
436 });
437 assert2::assert!(refused == want, "case {case}");
438 assert2::assert!(log.lso() == held, "case {case}");
439 assert2::assert!(
440 log.pending_transaction_start(ProducerId(1000)) == Some(Offset(0)),
441 "case {case}"
442 );
443 }
444 let reopened = open_log(dir.path());
445 assert2::assert!(reopened.lso() == Offset(0), "case {case}");
446 assert2::assert!(
447 reopened.pending_transaction_start(ProducerId(1000)) == Some(Offset(0)),
448 "case {case}"
449 );
450 }
451
452 #[test]
458 fn a_complete_transaction_holds_the_lso_until_the_high_watermark_passes_its_marker() {
459 struct Case {
460 name: &'static str,
461 transaction: bool,
462 high_watermarks: &'static [i64],
463 want: Offset,
464 }
465 let cases = [
466 Case {
467 name: "high watermark inside the transaction",
468 transaction: true,
469 high_watermarks: &[12],
470 want: Offset(10),
471 },
472 Case {
473 name: "high watermark at the marker",
474 transaction: true,
475 high_watermarks: &[15],
476 want: Offset(10),
477 },
478 Case {
479 name: "high watermark past the marker",
480 transaction: true,
481 high_watermarks: &[16],
482 want: Offset(16),
483 },
484 Case {
485 name: "high watermark moves past the marker in steps",
486 transaction: true,
487 high_watermarks: &[12, 15, 16],
488 want: Offset(16),
489 },
490 Case {
491 name: "no transaction",
492 transaction: false,
493 high_watermarks: &[16],
494 want: Offset(16),
495 },
496 ];
497 for case in cases {
498 let TempLog { dir: _dir, mut log } = test_log();
499 log.append(&mut sample_batch(10)).unwrap(); if case.transaction {
501 log.append(&mut transactional_batch(1000, 0, &[
502 "a", "b", "c", "d", "e",
503 ]))
504 .unwrap(); log.append(&mut commit_marker(1000, 0)).unwrap(); } else {
507 log.append(&mut sample_batch(6)).unwrap(); }
509 let mut got = Offset(-1);
510 for &high_watermark in case.high_watermarks {
511 got = log.last_stable_offset(Offset(high_watermark));
512 }
513 assert2::assert!(got == case.want, "case {}", case.name);
514 }
515 }
516
517 #[test]
522 fn a_reopened_log_holds_a_complete_transaction_until_the_high_watermark_passes() {
523 let dir = tempdir().unwrap();
524 {
525 let mut log = open_log(dir.path());
526 log.append(&mut sample_batch(10)).unwrap(); append_transaction(&mut log, 1000, 0, &["a", "b"]); log.append(&mut abort_marker(1000, 0)).unwrap(); }
530 let mut reopened = open_log(dir.path());
531 assert2::assert!(
532 [12, 13, 11].map(|hw| reopened.last_stable_offset(Offset(hw)))
533 == [Offset(10), Offset(13), Offset(11)]
534 );
535 }
536
537 #[test]
538 fn stale_pending_start_beyond_log_end_is_rejected() {
539 let TempLog { dir: _dir, mut log } = test_log();
540 log.pending.insert(ProducerId(1000), Offset(1));
541 let error = log.refresh_lso().unwrap_err();
542 assert2::assert!(let LogError::Corrupt(_) = error);
543 assert2::assert!(log.lso() == Offset(0));
544 }
545
546 #[test]
547 fn a_nonminimal_pending_start_beyond_log_end_is_rejected() {
548 let TempLog { dir: _dir, mut log } = test_log();
549 log.pending.insert(ProducerId(1000), Offset(0));
550 log.pending.insert(ProducerId(2000), Offset(1));
551 assert2::assert!(let Err(LogError::Corrupt(_)) = log.refresh_lso());
552 assert2::assert!(log.lso() == Offset(0));
553 }
554
555 #[yare::parameterized(
556 hundred = { 100 },
557 thousand = { 1_000 },
558 ten_thousand = { 10_000 },
559 )]
560 fn unchanged_pending_frontier_survives_many_ordinary_appends(producers: i64) {
561 let TempLog { dir: _dir, mut log } = test_log();
562 log.append(&mut sample_batch(i32::try_from(producers).unwrap()))
563 .unwrap();
564 for producer in 0..producers {
568 log.pending.insert(ProducerId(producer), Offset(producer));
569 }
570 for _ in 0..1_000 {
571 log.append(&mut sample_batch(1)).unwrap();
572 assert2::assert!(log.lso() == Offset(0));
573 }
574 }
575
576 #[test]
577 fn ordinary_appends_and_marker_release_match_the_scalar_frontier() {
578 let TempLog { dir: _dir, mut log } = test_log();
579 for producer in 0..100 {
580 append_transaction(&mut log, producer, 0, &["open"]);
581 }
582 for _ in 0..100 {
583 log.append(&mut sample_batch(1)).unwrap();
584 let starts: Vec<_> = log.pending.values().map(|offset| offset.0).collect();
585 assert2::assert!(
586 Some(log.lso().0)
587 == krabka_verified::first_unstable_offset(&starts, log.log_end_offset().0)
588 );
589 }
590 for producer in 0..100 {
591 log.append(&mut commit_marker(producer, 0)).unwrap();
592 let end = log.log_end_offset();
593 assert2::assert!(log.last_stable_offset(Offset(end.0 - 1)) <= Offset(end.0 - 1));
594 log.release_replicated_transactions(end);
595 let starts: Vec<_> = log.pending.values().map(|offset| offset.0).collect();
596 assert2::assert!(
597 Some(log.lso().0) == krabka_verified::first_unstable_offset(&starts, end.0)
598 );
599 }
600 }
601
602 #[derive(Clone, Copy)]
604 enum TxnStep {
605 Data(i64, usize),
607 Abort(i64),
608 Commit(i64),
609 Roll,
611 Replicate,
614 }
615
616 fn txn_layout(steps: &[TxnStep]) -> TempLog {
617 let TempLog { dir, mut log } = test_log();
618 for &step in steps {
619 match step {
620 TxnStep::Data(pid, records) => {
621 let values = vec!["v"; records];
622 append_transaction(&mut log, pid, 0, &values);
623 }
624 TxnStep::Abort(pid) => {
625 log.append(&mut abort_marker(pid, 0)).unwrap();
626 }
627 TxnStep::Commit(pid) => {
628 log.append(&mut commit_marker(pid, 0)).unwrap();
629 }
630 TxnStep::Roll => {
631 log.roll().unwrap();
632 }
633 TxnStep::Replicate => {
634 let end = log.log_end_offset();
635 log.release_replicated_transactions(end);
636 }
637 }
638 }
639 TempLog { dir, log }
640 }
641
642 fn aborted_by_full_scan(log: &Log, start: Offset, end: Offset) -> Vec<AbortedTxn> {
645 let mut aborted = Vec::new();
646 if let Some(first_base) = log
647 .segments
648 .iter()
649 .find(|segment| segment.last_offset() >= start)
650 .map(Segment::base_offset)
651 {
652 for index in log
653 .sealed_txn_indexes
654 .range(first_base..)
655 .map(|(_, index)| index)
656 {
657 aborted.extend(index.aborted_in_range(start, end).copied());
658 }
659 }
660 aborted.extend(log.active_txn_index.aborted_in_range(start, end).copied());
661 aborted
662 }
663
664 #[yare::parameterized(
669 one_abort_per_segment = { &[
670 TxnStep::Data(1, 3), TxnStep::Abort(1), TxnStep::Replicate, TxnStep::Roll,
671 TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate, TxnStep::Roll,
672 TxnStep::Data(1, 4), TxnStep::Abort(1), TxnStep::Replicate, TxnStep::Roll,
673 TxnStep::Data(3, 1), TxnStep::Abort(3), TxnStep::Replicate,
674 ] },
675 interleaved_producers = { &[
676 TxnStep::Data(1, 2), TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Roll,
677 TxnStep::Data(3, 2), TxnStep::Data(1, 1), TxnStep::Abort(1), TxnStep::Replicate,
678 TxnStep::Roll, TxnStep::Abort(3), TxnStep::Data(2, 3), TxnStep::Replicate,
679 TxnStep::Roll, TxnStep::Abort(2),
680 ] },
681 a_long_transaction_spans_segments = { &[
682 TxnStep::Data(9, 2), TxnStep::Data(1, 2), TxnStep::Abort(1), TxnStep::Replicate,
683 TxnStep::Roll, TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate,
684 TxnStep::Roll, TxnStep::Data(9, 2), TxnStep::Abort(9), TxnStep::Replicate,
685 TxnStep::Roll, TxnStep::Data(3, 2), TxnStep::Abort(3), TxnStep::Replicate,
686 ] },
687 commits_between_aborts_without_replication = { &[
688 TxnStep::Data(1, 2), TxnStep::Commit(1), TxnStep::Data(2, 2), TxnStep::Abort(2),
689 TxnStep::Roll, TxnStep::Data(1, 1), TxnStep::Abort(1), TxnStep::Roll,
690 TxnStep::Data(2, 2), TxnStep::Commit(2), TxnStep::Data(3, 2), TxnStep::Abort(3),
691 ] },
692 segments_without_aborts = { &[
693 TxnStep::Data(1, 2), TxnStep::Commit(1), TxnStep::Replicate, TxnStep::Roll,
694 TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate, TxnStep::Roll,
695 TxnStep::Data(3, 2), TxnStep::Commit(3), TxnStep::Replicate, TxnStep::Roll,
696 TxnStep::Data(4, 2), TxnStep::Commit(4), TxnStep::Replicate, TxnStep::Roll,
697 TxnStep::Data(5, 1), TxnStep::Abort(5),
698 ] },
699 )]
700 fn aborted_search_stops_without_changing_its_result(steps: &[TxnStep]) {
701 let TempLog { dir: _dir, log } = txn_layout(steps);
702 let end = log.log_end_offset().0;
703 let mut actual = Vec::new();
704 let mut expected = Vec::new();
705 for start in 0..=end + 1 {
706 for upper in 0..=end + 2 {
707 let (start, upper) = (Offset(start), Offset(upper));
708 actual.push(log.aborted_in_range(start, upper));
709 expected.push(aborted_by_full_scan(&log, start, upper));
710 }
711 }
712 assert2::assert!(actual == expected);
713 assert2::assert!(expected.iter().any(|aborted| !aborted.is_empty()));
714 }
715}