1#[cfg(not(target_os = "wasi"))]
9use std::collections::VecDeque;
10use {
11 super::{
12 Log,
13 control::{
14 ControlBatchKind, TransactionMarkerFlags, batch_control_marker_coordinator_epoch,
15 control_batch_kind, transaction_marker_flags,
16 },
17 pending_transactions::PendingTransactions,
18 },
19 crate::{
20 config::LogConfig,
21 error::LogError,
22 io::FileIo,
23 leader_epoch_checkpoint::LeaderEpochCheckpoint,
24 log_start_offset_checkpoint, name,
25 producer_snapshot::{self, LoadedSnapshot, NUM_BATCHES_TO_RETAIN, ProducerSnapshotEntry},
26 recovery::{deleted_orphan_recover, swap_orphan_recover},
27 segment::Segment,
28 stamp_index::OffsetRange,
29 txn_index::TxnIndex,
30 },
31 krabka_ids::{Offset, ProducerId},
32 krabka_protocol::records::RecordBatch,
33 krabka_verified::{ProducerReloadRange, increment_sequence},
34 std::{
35 collections::{BTreeMap, BTreeSet, HashMap},
36 fs,
37 path::Path,
38 sync::{Arc, PoisonError, RwLock, atomic::AtomicU64},
39 time::Instant,
40 },
41 tracing::{field, instrument},
42};
43
44#[derive(Debug, PartialEq, Eq)]
47pub(super) struct RecoveredBatchOffsets {
48 pub(super) last: Offset,
49 pub(super) advanced_to: Offset,
50}
51
52#[derive(Debug, Default, PartialEq, Eq)]
55pub(super) struct PendingStampRecoveryStats {
56 #[cfg(test)]
57 pub(super) batches: usize,
58 #[cfg(test)]
59 pub(super) bytes: usize,
60}
61
62impl PendingStampRecoveryStats {
63 fn record(&mut self, batch: &RecordBatch) {
64 #[cfg(test)]
65 {
66 self.batches = self.batches.saturating_add(1);
67 self.bytes = self.bytes.saturating_add(batch.encoded_len());
68 }
69 let _ = (self, batch);
72 }
73}
74
75#[derive(Debug, PartialEq, Eq)]
77pub(super) struct ProducerTail {
78 pub(super) last_sequence: i32,
79 pub(super) last_offset: Offset,
80}
81
82#[derive(Debug, Clone, Copy)]
84pub(super) struct DataProducerAppend {
85 pub(super) producer_id: ProducerId,
86 pub(super) producer_epoch: i16,
87 pub(super) base_sequence: i32,
88 pub(super) last_offset_delta: i32,
89 pub(super) base_offset: Offset,
90 pub(super) timestamp: i64,
91 pub(super) is_transactional: bool,
92}
93
94impl Log {
95 #[instrument(
101 level = "info",
102 skip_all,
103 fields(
104 dir = %dir.as_ref().display(),
105 segments = field::Empty,
106 log_end = field::Empty,
107 ),
108 err,
109 )]
110 #[cfg_attr(test, mutants::skip)]
115 pub fn open(dir: impl AsRef<Path>, config: LogConfig) -> Result<Self, LogError> {
118 let dir = dir.as_ref().to_path_buf();
119 fs::create_dir_all(&dir)?;
120
121 swap_orphan_recover(&dir)?;
124 deleted_orphan_recover(&FileIo, &dir)?;
128
129 let mut base_offsets: Vec<i64> = Vec::new();
130 for entry in fs::read_dir(&dir)? {
131 let entry = entry?;
132 let Ok(file_name) = entry.file_name().into_string() else {
133 continue; };
135 if let Ok(base) = name::parse_log_filename(&file_name) {
136 base_offsets.push(base);
137 }
138 }
139 base_offsets.sort_unstable();
140 base_offsets.dedup();
141 if !krabka_verified::local_recovery_segment_chain(&base_offsets) {
142 return Err(LogError::Corrupt(
143 "log segment bases are not a nonnegative ordered chain".into(),
144 ));
145 }
146
147 let mut segments: Vec<Segment> = Vec::with_capacity(base_offsets.len());
148 let mut active: Option<Segment> = None;
149 for (i, base) in base_offsets.iter().enumerate() {
150 if let Some(&next_base) = base_offsets.get(i + 1) {
151 let mut seg = Segment::open_unscanned(&dir, Offset(*base))?;
152 let last = krabka_verified::local_recovery_sealed_last(*base, next_base)
160 .ok_or_else(|| LogError::Corrupt("invalid sealed segment boundary".into()))?;
161 seg.seal_at(Offset(last));
162 seg.restore_max_timestamp()?;
168 segments.push(seg);
169 } else {
170 active = Some(Segment::open_active_with_index_interval(
171 &dir,
172 Offset(*base),
173 config.validate_on_open,
174 config.index_interval,
175 )?);
176 }
177 }
178
179 let (active, dir_sync_needed) = match active {
180 Some(s) => (s, true),
185 None => (Segment::create(&dir, Offset(0))?, true),
186 };
187
188 let sealed_txn_indexes = segments
189 .iter()
190 .map(|segment| {
191 Ok((
192 segment.base_offset(),
193 TxnIndex::open(segment.txn_index_path())?,
194 ))
195 })
196 .collect::<Result<BTreeMap<_, _>, LogError>>()?;
197 let active_txn_index = TxnIndex::open(active.txn_index_path())?;
198 let mut epoch_checkpoint =
199 LeaderEpochCheckpoint::open(active.leader_epoch_checkpoint_path())?;
200 let lso = active.last_offset() + 1;
202 epoch_checkpoint.truncate_from_end(lso)?;
203
204 let remote_storage_enable = config.remote_storage_enable;
205 let config = Arc::new(RwLock::new(config));
206
207 let span = tracing::Span::current();
208 span.record("segments", segments.len() + 1);
209 span.record("log_end", lso.0);
210
211 let start_offset = segments
215 .first()
216 .map_or_else(|| active.base_offset(), Segment::base_offset);
217
218 let mut log = Self {
219 dir,
220 config,
221 io: Arc::new(FileIo),
222 segments,
223 active: Some(active),
224 dir_sync_needed,
225 rollover_flusher: super::rollover_flush::Flusher::default(),
226 compaction_gate: Arc::default(),
227 fenced: false,
228 opened_end: Offset(0),
229 unflushed_messages: 0,
230 last_flush: Instant::now(),
231 #[cfg(not(target_os = "wasi"))]
232 writeback_unsynced: 0,
233 #[cfg(not(target_os = "wasi"))]
234 writeback_jobs: VecDeque::new(),
235 #[cfg(not(target_os = "wasi"))]
236 writeback_handed: 0,
237 truncations: Arc::new(AtomicU64::new(0)),
238 roll_jitter: None,
239 pending_deletes: Vec::new(),
240 start_offset,
241 start_offset_established: false,
245 checkpointed_start: None,
246 start_checkpoint_due: None,
247 clean_prefix_segments: 0,
250 lso,
251 pending: PendingTransactions::default(),
252 verification_states: HashMap::new(),
253 unreplicated: BTreeMap::new(),
254 pending_stamp_ranges: HashMap::new(),
255 coordinator_epochs: HashMap::new(),
256 producer_state: HashMap::new(),
257 earlier_batches: HashMap::new(),
258 active_txn_index,
259 sealed_txn_indexes,
260 stamp_source: None,
261 stamp_indexes: BTreeMap::new(),
262 epoch_checkpoint,
263 reconciled_frontier: Offset(0),
264 delivery_watermark: Offset(0),
265 delivery_pending_ms: None,
266 };
267 let checkpoint = log_start_offset_checkpoint::read(&log.dir)?;
271 log.checkpointed_start = checkpoint;
272 if let Some(checkpointed) = checkpoint {
273 let capped = checkpointed.min(log.log_end_offset());
278 let effective = if remote_storage_enable {
290 capped
291 } else {
292 capped.max(log.start_offset)
293 };
294 log.start_offset = effective;
295 log.start_offset_established = true;
299 if capped != checkpointed {
300 log.write_start_checkpoint(effective)?;
305 } else if effective != capped {
306 log.defer_start_checkpoint(effective, Instant::now());
310 }
311 log.epoch_checkpoint.truncate_from_start(effective)?;
315 }
316 let segment_bases: Vec<i64> = log
320 .segments
321 .iter()
322 .chain(log.active.iter())
323 .map(|segment| segment.base_offset().0)
324 .collect();
325 producer_snapshot::remove_strays(&log.dir, &segment_bases)?;
326 log.rebuild_producer_and_transaction_state()?;
327 log.delivery_watermark = log.log_start_offset();
330 log.opened_end = log.log_end_offset();
331 Ok(log)
332 }
333
334 pub(super) fn producer_reload_range(&self, log_end: Offset) -> ProducerReloadRange {
341 let local_start = self.local_log_start_offset();
342 ProducerReloadRange {
343 log_start: krabka_verified::producer_snapshot_reload_log_start(
344 self.config
345 .read()
346 .unwrap_or_else(PoisonError::into_inner)
347 .remote_storage_enable,
348 self.established_log_start().map(|start| start.0),
349 local_start.0,
350 ),
351 local_start: local_start.0,
352 log_end: log_end.0,
353 }
354 }
355
356 pub(super) fn rebuild_producer_and_transaction_state(&mut self) -> Result<(), LogError> {
367 self.clear_producer_and_transaction_state();
368 let end = self.log_end_offset();
369 let range = self.producer_reload_range(end);
370 let snapshot = producer_snapshot::reload(&self.dir, range)?;
371 let snapshot_offset = snapshot.as_ref().map(|loaded| loaded.offset.0);
372 if let Some(LoadedSnapshot { state, .. }) = snapshot {
373 self.producer_state = state;
374 for (&producer_id, entry) in &self.producer_state {
375 if let Some(first_offset) = entry.current_txn_first_offset {
376 self.pending.insert(producer_id, first_offset);
377 }
378 if entry.coordinator_epoch >= 0 {
379 self.coordinator_epochs
380 .insert(producer_id, entry.coordinator_epoch);
381 }
382 }
383 }
384 let mut next = krabka_verified::producer_snapshot_replay_start(range, snapshot_offset)
385 .map(Offset)
386 .ok_or_else(|| LogError::Corrupt("invalid producer snapshot replay frontier".into()))?;
387
388 let mut boundaries: BTreeSet<Offset> = self
389 .segments
390 .iter()
391 .map(Segment::base_offset)
392 .skip(1)
393 .chain(self.active.iter().map(Segment::base_offset))
394 .collect();
395 let mut boundaries = boundaries.split_off(&next);
396 let _ = boundaries.remove(&next);
397 let mut first_read = true;
398 while next < end {
399 let read = self.read(next, krabka_units::mebibytes(1))?;
400 if read.batches.is_empty() {
401 return Err(LogError::Corrupt(format!(
402 "producer-state recovery made no progress at offset {next}"
403 )));
404 }
405 let mut advanced_to = next;
406 for (index, batch) in read.batches.iter().enumerate() {
407 let cursor = Self::replay_cursor(first_read && index == 0, advanced_to, batch);
408 advanced_to = Self::recovered_batch_offsets(cursor, end, batch)?.advanced_to;
409 self.apply_recovered_batch_state(batch)?;
410 let covered: Vec<_> = boundaries.range(..=advanced_to).copied().collect();
411 for boundary in covered {
412 producer_snapshot::write(&*self.io, &self.dir, boundary, &self.producer_state)?;
413 let _ = boundaries.remove(&boundary);
414 }
415 }
416 if advanced_to <= next {
417 return Err(LogError::Corrupt(format!(
418 "producer-state recovery did not advance past offset {next}"
419 )));
420 }
421 next = advanced_to;
422 first_read = false;
423 }
424 if end.0 > range.log_start {
431 producer_snapshot::write(&*self.io, &self.dir, end, &self.producer_state)?;
432 }
433 let _ = self.rebuild_pending_stamp_ranges()?;
434 self.refresh_lso()?;
435 Ok(())
436 }
437
438 fn replay_cursor(first: bool, cursor: Offset, batch: &RecordBatch) -> Offset {
445 if first {
446 cursor.min(Offset(batch.base_offset))
447 } else {
448 cursor
449 }
450 }
451
452 fn apply_recovered_batch_state(&mut self, batch: &RecordBatch) -> Result<(), LogError> {
453 if control_batch_kind(batch) == Some(ControlBatchKind::Barrier) {
454 return Ok(());
457 }
458 let producer_id = ProducerId(batch.producer_id);
459 if producer_id.get() < 0 {
460 return Ok(());
461 }
462 self.update_owned_producer_entry(batch)?;
463 if batch.attributes.is_control_batch() {
464 let TransactionMarkerFlags {
465 is_abort,
466 is_commit,
467 } = transaction_marker_flags(batch)?;
468 if krabka_verified::transaction_marker_closes(
469 is_abort,
470 is_commit,
471 self.pending.contains_key(producer_id),
472 ) && let Some(first_offset) = self.pending.remove(producer_id)
473 {
474 self.unreplicated.insert(
475 first_offset,
476 Offset(batch.base_offset + i64::from(batch.last_offset_delta)),
477 );
478 }
479 if (is_abort || is_commit)
480 && let Some(epoch) = batch_control_marker_coordinator_epoch(batch)?
481 {
482 self.coordinator_epochs.insert(producer_id, epoch);
483 }
484 } else if batch.attributes.is_transactional() {
485 self.pending
486 .insert_first(producer_id, Offset(batch.base_offset));
487 }
488 Ok(())
489 }
490
491 fn rebuild_pending_stamp_ranges(&mut self) -> Result<PendingStampRecoveryStats, LogError> {
492 self.pending_stamp_ranges.clear();
493 if self.pending.is_empty() {
494 return Ok(PendingStampRecoveryStats::default());
495 }
496 let local_start = self.local_log_start_offset();
503 let mut next = self
504 .pending
505 .values()
506 .copied()
507 .min()
508 .unwrap_or(local_start)
509 .max(local_start);
510 let end = self.log_end_offset();
511 let mut is_first_read = true;
512 let mut stats = PendingStampRecoveryStats::default();
513 while next < end {
514 let slice = self.read(next, krabka_units::mebibytes(1))?;
515 if slice.batches.is_empty() {
516 let msg = format!("transaction-stamp recovery made no progress at offset {next}");
517 return Err(LogError::Corrupt(msg));
518 }
519 for (index, batch) in slice.batches.iter().enumerate() {
520 let producer_id = ProducerId(batch.producer_id);
521 let cursor = Self::replay_cursor(is_first_read && index == 0, next, batch);
522 let RecoveredBatchOffsets { last, advanced_to } =
523 Self::recovered_batch_offsets(cursor, end, batch)?;
524 stats.record(batch);
525 match control_batch_kind(batch) {
526 Some(ControlBatchKind::Barrier) => {}
529 Some(ControlBatchKind::Transaction) => {
530 self.pending_stamp_ranges.remove(&producer_id);
531 }
532 None => {
533 if batch.attributes.is_transactional()
534 && producer_id.get() >= 0
535 && self.pending.contains_key(producer_id)
536 {
537 self.pending_stamp_ranges
538 .entry(producer_id)
539 .or_default()
540 .push(OffsetRange {
541 first: Offset(batch.base_offset),
542 last,
543 });
544 }
545 }
546 }
547 next = advanced_to;
548 }
549 is_first_read = false;
550 }
551 Ok(stats)
552 }
553
554 fn recovered_batch_offsets(
555 current: Offset,
556 end: Offset,
557 batch: &RecordBatch,
558 ) -> Result<RecoveredBatchOffsets, LogError> {
559 match krabka_verified::replay_batch_cursor_decision(
560 current.0,
561 end.0,
562 Some((batch.base_offset, batch.last_offset_delta)),
563 ) {
564 krabka_verified::ReplayCursorDecision::Advance(next) => Ok(RecoveredBatchOffsets {
565 last: Offset(next - 1),
566 advanced_to: Offset(next),
567 }),
568 krabka_verified::ReplayCursorDecision::Stop => Err(LogError::Corrupt(format!(
569 "log recovery rejected batch at offset {current} before end {end}"
570 ))),
571 }
572 }
573
574 pub(super) fn update_owned_producer_entry(
575 &mut self,
576 batch: &RecordBatch,
577 ) -> Result<(), LogError> {
578 let producer_id = ProducerId(batch.producer_id);
579 if producer_id.get() < 0 {
580 return Ok(());
581 }
582 if batch.attributes.is_control_batch() {
583 let entry = self
584 .producer_state
585 .entry(producer_id)
586 .or_insert_with(|| ProducerSnapshotEntry::empty(producer_id, batch.producer_epoch));
587 if entry.producer_epoch != batch.producer_epoch {
588 self.earlier_batches.remove(&producer_id);
591 entry.last_sequence = -1;
592 entry.last_offset = Offset(-1);
593 entry.offset_delta = 0;
594 }
595 entry.producer_epoch = batch.producer_epoch;
596 entry.timestamp = batch.max_timestamp;
597 let TransactionMarkerFlags {
598 is_abort,
599 is_commit,
600 } = transaction_marker_flags(batch)?;
601 if is_abort || is_commit {
602 entry.current_txn_first_offset = None;
603 }
604 if (is_abort || is_commit)
605 && let Some(epoch) = batch_control_marker_coordinator_epoch(batch)?
606 {
607 entry.coordinator_epoch = epoch;
608 }
609 return Ok(());
610 }
611 self.update_data_producer_entry(DataProducerAppend {
612 producer_id,
613 producer_epoch: batch.producer_epoch,
614 base_sequence: batch.base_sequence,
615 last_offset_delta: batch.last_offset_delta,
616 base_offset: Offset(batch.base_offset),
617 timestamp: batch.max_timestamp,
618 is_transactional: batch.attributes.is_transactional(),
619 })
620 }
621
622 pub(super) fn update_data_producer_entry(
623 &mut self,
624 append: DataProducerAppend,
625 ) -> Result<(), LogError> {
626 let DataProducerAppend {
627 producer_id,
628 producer_epoch,
629 base_sequence,
630 last_offset_delta,
631 base_offset,
632 timestamp,
633 is_transactional,
634 } = append;
635 let Some(ProducerTail {
636 last_sequence,
637 last_offset,
638 }) = Self::data_producer_tail(producer_id, base_sequence, last_offset_delta, base_offset)?
639 else {
640 return Ok(());
641 };
642 let entry = self
643 .producer_state
644 .entry(producer_id)
645 .or_insert_with(|| ProducerSnapshotEntry::empty(producer_id, producer_epoch));
646 if entry.producer_epoch != producer_epoch {
650 self.earlier_batches.remove(&producer_id);
651 } else if let Some(previous) = entry.last_batch() {
652 let earlier = self.earlier_batches.entry(producer_id).or_default();
653 earlier.push_back(previous);
654 if earlier.len() >= NUM_BATCHES_TO_RETAIN {
655 earlier.pop_front();
656 }
657 }
658 entry.producer_epoch = producer_epoch;
659 entry.last_sequence = last_sequence;
660 entry.last_offset = last_offset;
661 entry.offset_delta = last_offset_delta;
662 entry.timestamp = timestamp;
663 if is_transactional && entry.current_txn_first_offset.is_none() {
664 entry.current_txn_first_offset = Some(base_offset);
665 }
666 Ok(())
667 }
668
669 pub(super) fn data_producer_tail(
670 producer_id: ProducerId,
671 base_sequence: i32,
672 last_offset_delta: i32,
673 base_offset: Offset,
674 ) -> Result<Option<ProducerTail>, LogError> {
675 if producer_id.get() < 0 || base_sequence < 0 {
676 return Ok(None);
677 }
678 if last_offset_delta < 0 {
679 return Err(LogError::InvalidArgument(format!(
680 "negative producer offset delta for producer {producer_id}"
681 )));
682 }
683 let last_sequence = increment_sequence(base_sequence, last_offset_delta);
684 let last_offset = base_offset
685 .0
686 .checked_add(i64::from(last_offset_delta))
687 .map(Offset)
688 .ok_or_else(|| {
689 LogError::InvalidArgument(format!(
690 "producer offset overflow for producer {producer_id}"
691 ))
692 })?;
693 Ok(Some(ProducerTail {
694 last_sequence,
695 last_offset,
696 }))
697 }
698}
699
700#[cfg(test)]
701mod tests;