krabka_log/log/append.rs
1//! The owned append paths, which decode-and-re-encode a `RecordBatch`, and
2//! the segment roll they share with every other writer.
3//!
4//! `append` assigns the next offset and `append_at` keeps a
5//! caller-supplied one; both funnel into one private helper so the LSO,
6//! the producer state, and the leader-epoch checkpoint move identically.
7
8use {
9 super::{
10 Log,
11 control::{ControlBatchKind, check_control_record_versions, control_batch_kind},
12 },
13 crate::{
14 config::{DeliveryPolicy, ScheduleOrder, SegmentAllocation},
15 error::LogError,
16 producer_snapshot, retention,
17 segment::Segment,
18 stamp_index::OffsetRange,
19 txn_index::TxnIndex,
20 },
21 bytes::{Bytes, BytesMut},
22 krabka_ids::{LeaderEpoch, Offset, ProducerId},
23 krabka_protocol::records::{RecordBatch, TimestampType},
24 krabka_units::prelude::{ByteSize, ByteSizeExt as _, Time, TimeExt as _},
25 std::{
26 mem,
27 sync::{Arc, PoisonError},
28 time::SystemTime,
29 },
30 tracing::{field, instrument},
31};
32
33/// The bytes an append stores: the caller's wire bytes, or the batch encoded
34/// here.
35enum StoredBytes<'a> {
36 Verbatim(&'a [u8]),
37 Encoded(Bytes),
38}
39
40impl StoredBytes<'_> {
41 fn as_slice(&self) -> &[u8] {
42 match self {
43 Self::Verbatim(bytes) => bytes,
44 Self::Encoded(bytes) => bytes,
45 }
46 }
47}
48
49/// The write settings an append retains after it rolls the active segment.
50pub(super) struct AppendRoll {
51 pub(super) index_interval: ByteSize,
52 pub(super) flush_on_append: bool,
53}
54
55impl Log {
56 pub(super) fn record_pending_transaction(
57 &mut self,
58 pid: ProducerId,
59 base_offset: Offset,
60 last_offset: Offset,
61 ) {
62 self.pending.insert_first(pid, base_offset);
63 self.pending_stamp_ranges
64 .entry(pid)
65 .or_default()
66 .push(OffsetRange {
67 first: base_offset,
68 last: last_offset,
69 });
70 }
71
72 pub(super) fn complete_append_state(
73 &mut self,
74 result: Result<(), LogError>,
75 base: Offset,
76 ) -> Result<(), LogError> {
77 if let Err(error) = result {
78 self.rollback_failed_append(base)?;
79 return Err(error);
80 }
81 Ok(())
82 }
83
84 /// Read one append policy, roll against this batch, reserve the segment
85 /// the batch lands in, and retain its write settings.
86 pub(super) fn roll_for_append(
87 &mut self,
88 incoming_size: impl FnOnce() -> ByteSize,
89 max_timestamp: i64,
90 ) -> Result<AppendRoll, LogError> {
91 let (
92 segment_size,
93 segment_roll_interval,
94 segment_index_size,
95 index_interval,
96 flush_on_append,
97 segment_allocation,
98 tail_cache_size,
99 ) = {
100 let cfg = self.config.read().unwrap_or_else(PoisonError::into_inner);
101 (
102 cfg.segment_size,
103 cfg.segment_roll_interval,
104 cfg.segment_index_size,
105 cfg.index_interval,
106 cfg.flush_on_append,
107 cfg.segment_allocation,
108 cfg.tail_cache_size,
109 )
110 };
111 let segment_roll_interval = self.jittered_roll_interval(segment_roll_interval);
112 if self.should_roll_for_incoming(
113 incoming_size(),
114 max_timestamp,
115 segment_size,
116 segment_roll_interval,
117 segment_index_size,
118 ) {
119 self.roll_active_segment()?;
120 }
121 // Kafka's `preallocate`, applied where every append passes rather
122 // than where segments are made: a topic's own config reaches a log
123 // only after it is opened, and a truncate takes a reservation away,
124 // so this is the one place that sees each segment under the config
125 // in force when it is written. A segment asks once for each; see
126 // `Segment::reserve` and `Segment::write_direct`.
127 if let Some(active) = self.active.as_mut() {
128 match segment_allocation {
129 SegmentAllocation::Preallocate => {
130 active.reserve(segment_size);
131 active.write_direct(tail_cache_size);
132 }
133 SegmentAllocation::OnWrite => active.write_buffered()?,
134 }
135 }
136 Ok(AppendRoll {
137 index_interval,
138 flush_on_append,
139 })
140 }
141
142 /// Append a `RecordBatch` and return the assigned `base_offset` together
143 /// with the log-append stamp the batch carries away from this call.
144 ///
145 /// The log overwrites the batch's `base_offset` with the next assigned
146 /// offset. `last_offset_delta` sets how many absolute offsets this batch
147 /// consumes.
148 ///
149 /// The second element is `Some(ms)` exactly when the partition's
150 /// `message.timestamp.type` is `LogAppendTime`, and it is the broker clock
151 /// reading this append stamped into the batch. It is Kafka's
152 /// `LogAppendInfo.logAppendTime`, which `ProduceResponse.logAppendTimeMs`
153 /// reports; `None` is a `CreateTime` partition, which Kafka answers with
154 /// `-1`.
155 #[instrument(
156 level = "debug",
157 skip_all,
158 fields(assigned_base = field::Empty, leader_epoch = batch.partition_leader_epoch),
159 err,
160 )]
161 /// # Errors
162 /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
163 pub fn append(&mut self, batch: &mut RecordBatch) -> Result<(Offset, Option<i64>), LogError> {
164 // A fenced log refuses every append with `Fenced`, before any of
165 // the append's own validation can report a different error.
166 self.ensure_unfenced()?;
167 // KFC-1, before an offset is assigned and before anything is written,
168 // so a refused batch leaves the log exactly as it found it.
169 self.reject_backwards_schedule(batch.max_timestamp)?;
170 // `partition_leader_epoch` is the raw KIP-320 wire `int32`; wrap it into
171 // the domain newtype at this boundary.
172 let leader_epoch = LeaderEpoch(batch.partition_leader_epoch);
173 let assigned_base = self.append_at_expected_offset();
174 tracing::Span::current().record("assigned_base", assigned_base.0);
175 batch.base_offset = assigned_base.0;
176 let log_append_time_ms = self.stamp_owned_log_append_time(batch);
177 self.append_preserving_offset(batch, None)?;
178 self.assign_appended_epoch(leader_epoch, assigned_base)?;
179 Ok((assigned_base, log_append_time_ms))
180 }
181
182 /// Record the leader epoch stamped on a batch just appended at `base`:
183 /// Kafka's `UnifiedLog.append`, which calls
184 /// `assignEpochStartOffset(batch.partitionLeaderEpoch, batch.baseOffset)`
185 /// for every appended batch.
186 ///
187 /// [`LeaderEpochCheckpoint::assign`](crate::LeaderEpochCheckpoint::assign)
188 /// decides what that changes: nothing for the latest epoch at or after
189 /// its start, and otherwise a new entry that first drops every trailing
190 /// entry it does not strictly follow. That also replaces an entry no
191 /// record backs, such as a leader epoch recorded at the log end by a
192 /// promotion that never wrote.
193 ///
194 /// Kafka stamps every batch it appends with a real epoch. This log also
195 /// takes internal and test writes that carry the `-1` "no epoch"
196 /// sentinel, and those record nothing, where Kafka's `assign` would throw.
197 ///
198 /// A checkpoint that cannot be written rolls the batch back, so the log
199 /// never holds a batch its epoch history does not account for.
200 pub(super) fn assign_appended_epoch(
201 &mut self,
202 leader_epoch: LeaderEpoch,
203 base: Offset,
204 ) -> Result<(), LogError> {
205 if !leader_epoch.is_known() {
206 return Ok(());
207 }
208 let result = self.epoch_checkpoint.assign(leader_epoch, base);
209 self.complete_append_state(result, base)
210 }
211
212 /// Refuse a batch that would make a scheduled partition's schedule run
213 /// backwards, which is KFC-1's `delivery.schedule.monotonic`.
214 ///
215 /// `delivery_ms` is the batch's `max_timestamp`, which on a
216 /// [`DeliveryPolicy::Scheduled`] partition is the time its records become
217 /// visible. KFC-1 defines the rejection against "the largest delivery time
218 /// already in the partition", and the log answers that as an existence
219 /// query: one record scheduled strictly after this batch is one record
220 /// this batch would hold up.
221 ///
222 /// Visibility is offset-ordered for a classic group, because a group's
223 /// position is one offset and a record it reads past is unreachable for it
224 /// forever. A batch that comes due before an earlier one therefore stalls
225 /// everything behind it rather than overtaking it, and the setting turns
226 /// that silent stall into an error at the producer that caused it.
227 ///
228 /// The two offset-assigning leader appends call this, and they call it
229 /// under the same lock acquisition that writes the batch. That is the
230 /// whole point of the check living here: a test taken before the append
231 /// admits two producers, or two jobs of one writer group, that then land
232 /// out of order.
233 ///
234 /// [`Log::offset_for_timestamp`] skips a segment whose own cached maximum
235 /// sits below the target, so a schedule that runs forward — the accepted
236 /// case — costs one integer comparison per segment and no disk read. Only
237 /// a rejected batch pays for an index lookup and a bounded scan. An
238 /// immediate partition, and a scheduled one that did not ask for the
239 /// setting, read two config fields and stop.
240 ///
241 /// # Errors
242 /// Returns [`LogError::ScheduleRunsBackwards`] when the partition already
243 /// holds a record whose delivery time is strictly after `delivery_ms`.
244 pub(super) fn reject_backwards_schedule(&self, delivery_ms: i64) -> Result<(), LogError> {
245 let monotonic = {
246 let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
247 config.delivery_policy == DeliveryPolicy::Scheduled
248 && config.schedule_order == ScheduleOrder::Monotonic
249 };
250 if !monotonic {
251 return Ok(());
252 }
253 // Nothing can be scheduled after `i64::MAX`, so a batch that names it
254 // is never the one that runs backwards.
255 let Some(later) = delivery_ms.checked_add(1) else {
256 return Ok(());
257 };
258 if self.offset_for_timestamp(later).is_some() {
259 return Err(LogError::ScheduleRunsBackwards { delivery_ms });
260 }
261 Ok(())
262 }
263
264 /// Stamp Kafka's log-append time onto an owned batch, when the partition
265 /// asks for it, and report the stamp.
266 ///
267 /// This is `LogValidator`'s `batch.setMaxTimestamp(LOG_APPEND_TIME, now)`:
268 /// on a `message.timestamp.type=LogAppendTime` partition every batch
269 /// carries the broker's clock at append instead of the producer's own
270 /// timestamps. Exactly two header fields move, the timestamp-type
271 /// attribute bit and `max_timestamp`; `base_timestamp` and the per-record
272 /// deltas stay as the producer wrote them, and a reader substitutes
273 /// `max_timestamp` for every record while the bit is set. The owned path
274 /// re-encodes the batch on the way to the segment, so its CRC follows on
275 /// its own.
276 ///
277 /// Only the offset-assigning append paths call this. Kafka runs
278 /// `LogValidator` under `validateAndAssignOffsets`, which is exactly the
279 /// leader append, and never on `appendAsFollower`: a follower stores the
280 /// leader's bytes as they arrived, already stamped, and re-stamping them
281 /// with the follower's own clock would give the two replicas different
282 /// bytes for one offset.
283 fn stamp_owned_log_append_time(&self, batch: &mut RecordBatch) -> Option<i64> {
284 let now = self.log_append_time_stamp()?;
285 batch.attributes = batch
286 .attributes
287 .with_timestamp_type(TimestampType::LogAppendTime);
288 batch.max_timestamp = now;
289 Some(now)
290 }
291
292 /// The broker clock reading this append stamps, or `None` on a
293 /// `CreateTime` partition, which stamps nothing.
294 pub(super) fn log_append_time_stamp(&self) -> Option<i64> {
295 let log_append_time = self
296 .config
297 .read()
298 .unwrap_or_else(PoisonError::into_inner)
299 .message_timestamp_type
300 == TimestampType::LogAppendTime;
301 log_append_time.then(|| retention::now_ms(SystemTime::now()))
302 }
303
304 /// Append a commit-marker batch with a coordinator-supplied transaction
305 /// stamp and return its assigned base offset.
306 ///
307 /// The stamp is internal metadata. It is not encoded into the marker or
308 /// any client-facing bytes. The marker must be a COMMIT control batch and
309 /// a [`crate::StampSource`] must already be installed.
310 ///
311 /// # Errors
312 /// Returns an error for a non-commit batch, a missing stamp source, or any
313 /// log/index I/O failure.
314 pub fn append_with_commit_stamp(
315 &mut self,
316 batch: &mut RecordBatch,
317 stamp: u64,
318 ) -> Result<Offset, LogError> {
319 // A fenced log refuses every append with `Fenced`, before any of
320 // the append's own validation can report a different error.
321 self.ensure_unfenced()?;
322 self.validate_commit_stamp_batch(batch)?;
323 let assigned_base = self.append_at_expected_offset();
324 batch.base_offset = assigned_base.0;
325 // A marker is an offset-assigning append too. Kafka runs the same
326 // `LogValidator` over an `AppendOrigin.COORDINATOR` append as over a
327 // client one, so a `LogAppendTime` partition stamps its markers as
328 // well as its data batches.
329 self.stamp_owned_log_append_time(batch);
330 self.append_preserving_offset(batch, Some(stamp))?;
331 self.assign_appended_epoch(LeaderEpoch(batch.partition_leader_epoch), assigned_base)?;
332 self.observe_stamp(stamp);
333 Ok(assigned_base)
334 }
335
336 pub(super) fn append_at_expected_offset(&self) -> Offset {
337 self.log_end_offset().max(self.reconciled_frontier)
338 }
339
340 /// Kafka's `UnifiedLog.append` check for an append that takes its offsets
341 /// as given (`appendAsFollower`): the first offset must not be below the
342 /// log end offset, so a batch that starts below it is a duplicate or a
343 /// divergence. A batch that starts above it is fine. Compaction leaves
344 /// holes in the leader's offsets, and a follower has to take the batches
345 /// on either side of one.
346 pub(super) fn admit_append_at(&self, offset: Offset) -> Result<(), LogError> {
347 let expected = self.append_at_expected_offset();
348 if offset < expected {
349 return Err(LogError::OffsetMismatch {
350 expected,
351 actual: offset,
352 });
353 }
354 Ok(())
355 }
356
357 /// Whether this follower log must restart at `batch_base` before it can
358 /// take the leader batch `[batch_base, batch_last]`: Kafka's
359 /// `Partition.appendRecordsToFollowerOrFutureReplica` recovery from
360 /// `UnexpectedAppendOffsetException`.
361 ///
362 /// A leader serves the whole batch that holds the fetch offset, so a
363 /// follower that restarted at a leader log start inside a batch --
364 /// `DeleteRecords`, retention, or a diskless flusher's trim put it there
365 /// -- receives a batch that begins below its own log end. When the log
366 /// holds nothing (`log_start == log_end`) and the batch spans its end,
367 /// restarting it at the batch's base loses nothing, and the batch then
368 /// appends where it begins. The follower raises its log start to the
369 /// leader's again from a later fetch response. A log that holds records
370 /// keeps the [`Log::append_at`] refusal, as Kafka rethrows there.
371 #[must_use]
372 pub fn restarts_at_batch_base(&self, batch_base: Offset, batch_last: Offset) -> bool {
373 let end = self.log_end_offset();
374 self.log_start_offset() == end && batch_base < end && end <= batch_last
375 }
376
377 /// Append a `RecordBatch` whose `base_offset` the caller sets.
378 ///
379 /// Unlike [`Log::append`], this method does NOT overwrite
380 /// `batch.base_offset`. The broker's replicator uses it to keep the
381 /// leader-assigned offset on the follower's local log.
382 ///
383 /// `offset` must not be below the log's current [`Log::log_end_offset`]
384 /// (or its reconciled frontier). If it is, this method returns
385 /// [`LogError::OffsetMismatch`]. An `offset` above the end is a hole in
386 /// the offsets, as a compacted leader's log has, and is appended as it
387 /// is. On success the log sets `batch.base_offset` to `offset`, which
388 /// should already match, before it writes the batch.
389 #[instrument(
390 level = "debug",
391 skip(self, batch),
392 fields(leader_epoch = batch.partition_leader_epoch),
393 err,
394 )]
395 /// # Errors
396 /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
397 pub fn append_at(&mut self, batch: &mut RecordBatch, offset: Offset) -> Result<(), LogError> {
398 // A fenced log refuses every append with `Fenced`, before any of
399 // the append's own validation can report a different error.
400 self.ensure_unfenced()?;
401 self.admit_append_at(offset)?;
402 // `partition_leader_epoch` is the raw KIP-320 wire `int32`; wrap it here.
403 let leader_epoch = LeaderEpoch(batch.partition_leader_epoch);
404 batch.base_offset = offset.0;
405 self.append_preserving_offset(batch, None)?;
406 // The follower records replicated epochs exactly as the leader does.
407 self.assign_appended_epoch(leader_epoch, offset)?;
408 Ok(())
409 }
410
411 /// Append a replicated commit marker at `offset` with its internal commit
412 /// stamp. This is the follower counterpart of
413 /// [`Log::append_with_commit_stamp`].
414 ///
415 /// # Errors
416 /// Returns an error for an offset mismatch, a non-commit batch, a missing
417 /// stamp source, or any log/index I/O failure.
418 pub fn append_at_with_commit_stamp(
419 &mut self,
420 batch: &mut RecordBatch,
421 offset: Offset,
422 stamp: u64,
423 ) -> Result<(), LogError> {
424 // A fenced log refuses every append with `Fenced`, before any of
425 // the append's own validation can report a different error.
426 self.ensure_unfenced()?;
427 self.admit_append_at(offset)?;
428 self.validate_commit_stamp_batch(batch)?;
429 batch.base_offset = offset.0;
430 self.append_preserving_offset(batch, Some(stamp))?;
431 self.assign_appended_epoch(LeaderEpoch(batch.partition_leader_epoch), offset)?;
432 self.observe_stamp(stamp);
433 Ok(())
434 }
435
436 /// Internal helper shared by [`Log::append`] and [`Log::append_at`].
437 ///
438 /// This function rolls the segment if necessary, appends to the active
439 /// segment, and honors `config.flush_on_append`. It does NOT reassign
440 /// `batch.base_offset`. Callers must set that field first. It also
441 /// updates the LSO and the active `.txnindex` from the batch attributes.
442 fn append_preserving_offset(
443 &mut self,
444 batch: &RecordBatch,
445 transaction_stamp: Option<u64>,
446 ) -> Result<(), LogError> {
447 self.append_preserving_offset_inner(batch, transaction_stamp, None)
448 }
449
450 /// Shared semantic bookkeeping; control passthrough supplies its original
451 /// wire bytes so decoding its marker does not re-encode the stored batch.
452 pub(super) fn append_preserving_offset_inner(
453 &mut self,
454 batch: &RecordBatch,
455 transaction_stamp: Option<u64>,
456 verbatim: Option<&[u8]>,
457 ) -> Result<(), LogError> {
458 self.ensure_unfenced()?;
459 self.rollover_flusher.check()?;
460 let base_offset = Offset(batch.base_offset);
461 let Some((last_offset, _)) = krabka_verified::local_append_coordinates(
462 self.append_at_expected_offset().0,
463 batch.base_offset,
464 batch.last_offset_delta,
465 ) else {
466 return Err(LogError::InvalidArgument(
467 "batch does not form a valid interval at the append frontier".into(),
468 ));
469 };
470 let last_offset = Offset(last_offset);
471 Self::data_producer_tail(
472 ProducerId(batch.producer_id),
473 batch.base_sequence,
474 batch.last_offset_delta,
475 Offset(batch.base_offset),
476 )?;
477 check_control_record_versions(batch)?;
478 let stored = if let Some(bytes) = verbatim {
479 StoredBytes::Verbatim(bytes)
480 } else {
481 let level = self
482 .config
483 .read()
484 .unwrap_or_else(PoisonError::into_inner)
485 .compression_level(batch.attributes.compression());
486 let mut bytes = BytesMut::with_capacity(batch.encoded_len());
487 batch.encode_with_compression_level(&mut bytes, level)?;
488 StoredBytes::Encoded(bytes.freeze())
489 };
490 let stored_bytes = stored.as_slice();
491 let AppendRoll {
492 index_interval,
493 flush_on_append,
494 } = self.roll_for_append(
495 || ByteSize::from_bytes(u64::try_from(stored_bytes.len()).unwrap_or(u64::MAX)),
496 batch.max_timestamp,
497 )?;
498
499 let result = (|| {
500 let active = self.active.as_mut().ok_or_else(super::no_active_segment)?;
501 match &stored {
502 StoredBytes::Verbatim(bytes) => {
503 active.append_verbatim(
504 bytes,
505 base_offset,
506 batch.last_offset_delta,
507 batch.max_timestamp,
508 LeaderEpoch(batch.partition_leader_epoch),
509 index_interval,
510 )?;
511 }
512 StoredBytes::Encoded(encoded) => {
513 active.append_encoded(batch, index_interval, encoded)?;
514 }
515 }
516
517 let pid = ProducerId(batch.producer_id);
518 let is_transactional = batch.attributes.is_transactional() && pid.get() >= 0;
519 let control_kind = control_batch_kind(batch);
520 let writes_durable_sidecar =
521 matches!(control_kind, Some(ControlBatchKind::Transaction))
522 || (self.stamp_source.is_some() && control_kind.is_none() && !is_transactional);
523 self.flush_after_append(
524 batch.last_offset_delta,
525 flush_on_append || writes_durable_sidecar,
526 )?;
527 #[cfg(not(target_os = "wasi"))]
528 self.pace_writeback(stored_bytes.len())?;
529
530 // Sidecars are written only after the batch bytes. Any failure in
531 // this block takes the full-log rollback path below.
532 if !batch.attributes.is_control_batch() && !is_transactional {
533 self.record_stamp(base_offset, last_offset)?;
534 }
535
536 let is_barrier = match control_kind {
537 Some(ControlBatchKind::Barrier) => {
538 self.refresh_lso()?;
539 true
540 }
541 Some(ControlBatchKind::Transaction) => {
542 self.apply_transaction_marker(batch, pid, last_offset, transaction_stamp)?;
543 self.refresh_lso()?;
544 false
545 }
546 None if is_transactional => {
547 self.record_pending_transaction(pid, base_offset, last_offset);
548 self.refresh_lso()?;
549 false
550 }
551 None => {
552 self.refresh_lso()?;
553 false
554 }
555 };
556
557 if !is_barrier {
558 self.update_owned_producer_entry(batch)?;
559 if is_transactional {
560 self.clear_verification_after_append(
561 pid,
562 batch.producer_epoch,
563 batch.attributes.is_control_batch(),
564 );
565 }
566 }
567 Ok(())
568 })();
569
570 self.complete_append_state(result, base_offset)
571 }
572
573 /// Kafka's `LogSegment.shouldRoll`, checked against an incoming batch
574 /// rather than the wall clock: this is what makes rolling an append-time
575 /// decision.
576 ///
577 /// True when appending `incoming_size` bytes would push the active
578 /// segment past `segment_size`, or when the active segment already holds
579 /// a record and the gap between `incoming_max_timestamp` and that
580 /// record's timestamp exceeds `segment_roll_interval`. An empty active
581 /// segment never rolls on age alone, the same as Kafka's `size > 0`
582 /// guard on `timeWaitedForRoll` -- there is no first record yet to
583 /// measure the gap from, and nothing (not even the wall clock) rolls an
584 /// idle partition.
585 ///
586 /// It is also true when the offset index or the time index already holds
587 /// as many entries as `segment_index_size` fits, Kafka's
588 /// `offsetIndex().isFull() || timeIndex().isFull()`.
589 pub(super) fn should_roll_for_incoming(
590 &mut self,
591 incoming_size: ByteSize,
592 incoming_max_timestamp: i64,
593 segment_size: ByteSize,
594 segment_roll_interval: Time,
595 segment_index_size: ByteSize,
596 ) -> bool {
597 let Some(seg) = self.active.as_mut() else {
598 return false;
599 };
600 // An empty active segment never rolls, on size or on age: there is
601 // nothing in it worth sealing, and rolling it would try to open a
602 // second segment at the same base offset. A batch bigger than
603 // `segment.bytes` still lands whole in an empty segment and makes
604 // its own oversized one, the same as Kafka never splitting a batch.
605 let non_empty = seg.size() > ByteSize::ZERO;
606 let size_roll = non_empty && seg.size() + incoming_size > segment_size;
607 let time_roll = non_empty
608 && seg.first_record_timestamp().is_some_and(|first_timestamp| {
609 incoming_max_timestamp.saturating_sub(first_timestamp)
610 > segment_roll_interval.millis_i64_trunc()
611 });
612 // Below 24 bytes of `segment.index.bytes` the time index is full
613 // before it holds an entry. Kafka then rolls an empty segment, which
614 // deletes and recreates it at the same base offset. Skipping that
615 // roll leaves the same segment in place.
616 let index_roll = non_empty && seg.indexes_full(segment_index_size);
617 size_roll || time_roll || index_roll
618 }
619
620 #[instrument(
621 level = "info",
622 skip_all,
623 fields(new_base = field::Empty),
624 err,
625 )]
626 pub(super) fn roll_active_segment(&mut self) -> Result<(), LogError> {
627 self.rollover_flusher.check()?;
628 let new_base = self.log_end_offset();
629 tracing::Span::current().record("new_base", new_base.0);
630 let buffered = !self
631 .config
632 .read()
633 .unwrap_or_else(PoisonError::into_inner)
634 .flush_on_append
635 && !cfg!(target_os = "wasi");
636 #[cfg(not(target_os = "wasi"))]
637 let flush = if buffered {
638 // Reserve both retained bytes and encoding scratch space before
639 // allocating the snapshot or cloning index descriptors.
640 let permit = self
641 .rollover_flusher
642 .reserve(producer_snapshot::allocation_size(
643 self.producer_state.len(),
644 )?)?;
645 let snapshot = producer_snapshot::prepare(&self.dir, new_base, &self.producer_state)?;
646 let files = self
647 .active
648 .as_ref()
649 .ok_or_else(super::no_active_segment)?
650 .flush_handles()?;
651 Some(super::rollover_flush::Flush {
652 files,
653 io: Arc::clone(&self.io),
654 snapshot,
655 _permit: permit,
656 })
657 } else {
658 None
659 };
660 if !buffered {
661 self.rollover_flusher.finish()?;
662 self.active_segment_flush()?;
663 producer_snapshot::write(&*self.io, &self.dir, new_base, &self.producer_state)?;
664 }
665 // Sealing writes the segment's last time-index entry, which can fail,
666 // so it runs before the segment leaves `self.active`.
667 self.active
668 .as_mut()
669 .ok_or_else(super::no_active_segment)?
670 .seal()?;
671 #[cfg(not(target_os = "wasi"))]
672 if let Some(flush) = flush {
673 // Once sealed, the boundary must be flushed even if any subsequent
674 // segment, transaction-index, or stamp-index setup fails.
675 self.rollover_flusher.submit(flush)?;
676 }
677 let old = self.active.take().ok_or_else(super::no_active_segment)?;
678 let old_base = old.base_offset();
679 self.segments.push(old);
680 let mut new_seg = Segment::create(&self.dir, new_base)?;
681 new_seg.set_io(Arc::clone(&self.io));
682 let new_txn_index = TxnIndex::open(new_seg.txn_index_path())?;
683 let old_txn_index = mem::replace(&mut self.active_txn_index, new_txn_index);
684 self.sealed_txn_indexes.insert(old_base, old_txn_index);
685 let stamp_index_path = new_seg.stamp_index_path();
686 self.active = Some(new_seg);
687 self.roll_jitter = None;
688 // The rollover flush syncs what the sealed segment's background
689 // syncs had not reached; the new segment starts with nothing unsynced.
690 #[cfg(not(target_os = "wasi"))]
691 self.restart_writeback_pacing(0);
692 self.dir_sync_needed = true;
693 self.reopen_active_stamp_index(new_base, stamp_index_path)?;
694 Ok(())
695 }
696}
697
698#[cfg(test)]
699mod tests;