Skip to main content

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;