Skip to main content

krabka_log/log/
state.rs

1//! Log-level accessors and the whole-log reset.
2//!
3//! These are the coordinates the broker reads off a partition -- the log
4//! start and end, the size, the last stable offset, the producer and
5//! transaction state -- together with the config swap and the hard reset
6//! that empties the log at a new base offset.
7
8#[cfg(any(test, feature = "test-helpers"))]
9use crate::io::LogIo;
10use {
11    super::Log,
12    crate::{
13        DeliveryPolicy, RecoveredProducer, SegmentAllocation, VerbatimBatch,
14        config::LogConfig,
15        error::LogError,
16        leader_epoch_checkpoint::LeaderEpochCheckpoint,
17        log_start_offset_checkpoint,
18        producer_snapshot::{self, ProducerSnapshotEntry},
19        segment::Segment,
20        txn_index::{AbortedTxn, TxnIndex},
21    },
22    krabka_ids::{Offset, ProducerId},
23    krabka_protocol::records::{Attributes, CRC_COVERAGE_START, HEADER_LEN, TimestampType},
24    krabka_units::prelude::{ByteSize, ByteSizeExt},
25    std::{
26        iter,
27        path::Path,
28        sync::{Arc, PoisonError, atomic::Ordering},
29        time::{Instant, SystemTime},
30    },
31    tracing::instrument,
32};
33
34/// When [`Log::advance_log_start_offset`] writes `log-start-offset-checkpoint`.
35#[derive(Clone, Copy)]
36enum StartCheckpoint {
37    /// Before it returns.
38    Now,
39    /// After [`LogConfig::start_offset_checkpoint_interval`], counted
40    /// from this instant.
41    Deferred(Instant),
42}
43
44impl Log {
45    /// Empty the volatile state before a hard reset or a durable producer-state reload.
46    pub(super) fn clear_producer_and_transaction_state(&mut self) {
47        self.pending.clear();
48        self.verification_states.clear();
49        self.unreplicated.clear();
50        self.pending_stamp_ranges.clear();
51        self.coordinator_epochs.clear();
52        self.producer_state.clear();
53        self.earlier_batches.clear();
54    }
55
56    /// Directory this log was opened against. The broker's intra-broker
57    /// log-dir reassignment (KIP-113) reads this to find the current owning
58    /// `log.dir` of a partition. The broker does not have to repeat the
59    /// directory-layout convention.
60    #[must_use]
61    pub fn dir(&self) -> &Path { &self.dir }
62
63    /// First absolute offset any reader may ask for, wherever the records for
64    /// it live: Kafka's global `logStartOffset`.
65    ///
66    /// On a tiered topic (KIP-405) this can sit below
67    /// [`Log::local_log_start_offset`], and the offsets between the two are
68    /// the ones the remote tier serves. A fetch below *this* floor is
69    /// `OFFSET_OUT_OF_RANGE` and no tier answers it.
70    #[must_use]
71    pub fn log_start_offset(&self) -> Offset { self.start_offset }
72
73    /// The global floor, when this process is the one that moved it.
74    ///
75    /// `None` on a log whose floor was only inferred from the segments present
76    /// at [`Log::open`]. A caller that would *refuse* or *delete* on the
77    /// strength of the floor must use this rather than
78    /// [`Log::log_start_offset`]: on a tiered partition whose local segments
79    /// were evicted, the inferred floor sits above everything the archive
80    /// holds, so refusing below it hides readable records and deleting below
81    /// it destroys them. The log-start checkpoint carries an established floor
82    /// across a restart, so a reopened log answers `None` only when nobody has
83    /// moved it.
84    #[must_use]
85    pub fn established_log_start(&self) -> Option<Offset> {
86        self.start_offset_established.then_some(self.start_offset)
87    }
88
89    /// The global floor as a leader reports it to the replicas that follow
90    /// it: Kafka's `UnifiedLog.logStartOffset` in a `Fetch` response.
91    ///
92    /// This is [`Log::log_start_offset`] except on a remote-storage-enabled
93    /// log whose floor nobody established. Such a log reopened over the
94    /// segments local retention left, so its inferred start sits above
95    /// everything the archive holds. Kafka's `LogLoader.load` never infers a
96    /// remote-storage-enabled log's start from its segments: it takes the
97    /// log-start checkpoint, which is 0 when nobody moved it. A follower
98    /// raises its own start to what its leader reports, and a follower's start
99    /// is a floor it refuses reads below and deletes the archive below once it
100    /// leads, so reporting the inferred start would hide and then destroy
101    /// every archived record under it.
102    #[must_use]
103    pub fn reported_log_start(&self) -> Offset {
104        match self.established_log_start() {
105            Some(start) => start,
106            None if self
107                .config
108                .read()
109                .unwrap_or_else(PoisonError::into_inner)
110                .remote_storage_enable =>
111            {
112                Offset(0)
113            }
114            None => self.start_offset,
115        }
116    }
117
118    /// The epoch of the earliest leader-epoch entry, when a log start that
119    /// somebody established truncated the cache to it: Kafka's
120    /// `leaderEpochCache.earliestEntry` after `truncateFromStart(logStartOffset)`.
121    ///
122    /// Only then does the entry say something about the remote tier. Kafka's
123    /// `RemoteLogManager` deletes a remote segment whose epochs all lie below
124    /// it (`deleteLogSegmentsDueToLeaderEpochCacheTruncation`), which is right
125    /// for the epoch that owns the log start, because every offset from there
126    /// on has that epoch or a later one. An earliest entry that no advance of
127    /// the log start put there (a cache that never held the older epochs, or
128    /// one cleared by [`Log::reset_to`] and refilled from the fetched
129    /// records) says nothing about them, so this answers `None`. The epoch
130    /// cache is cut from the start only by [`Log::set_log_start_offset`] and
131    /// by the log-start checkpoint [`Log::open`] restores, never by the
132    /// segments a tiered partition still holds locally, so a restart does not
133    /// move it.
134    #[must_use]
135    pub fn log_start_epoch(&self) -> Option<krabka_ids::LeaderEpoch> {
136        let start = self.established_log_start()?;
137        self.epoch_checkpoint
138            .entries()
139            .first()
140            .filter(|entry| entry.start_offset == start)
141            .map(|entry| entry.epoch)
142    }
143
144    /// The base offset of the log's first segment on disk, sealed or active,
145    /// or `None` when it holds none: Kafka's `logSegments().get(0).baseOffset()`.
146    #[must_use]
147    pub fn first_segment_base(&self) -> Option<Offset> {
148        self.segments
149            .first()
150            .or(self.active.as_ref())
151            .map(Segment::base_offset)
152    }
153
154    /// The first offset the segments on disk begin at, before the global
155    /// floor is applied.
156    pub(super) fn first_local_offset(&self) -> Offset {
157        self.segments
158            .first()
159            .or(self.active.as_ref())
160            .map_or(Offset(0), Segment::base_offset)
161    }
162
163    /// Move the global log start up to `new_start`.
164    ///
165    /// The pointer only ever moves forward, the way Kafka's
166    /// `maybeIncrementLogStartOffset` does: a request that names an offset at
167    /// or below the current floor is a no-op. `trim_to_offset` uses this
168    /// method for the active-segment case, the broker's `DeleteRecords`
169    /// handler uses it, and the `RemoteLogManager` uses it after a remote
170    /// segment is deleted. This method does NOT truncate on-disk segments. It
171    /// only moves the start pointer.
172    ///
173    /// The new value is checkpointed to `log-start-offset-checkpoint` before
174    /// this method returns, so a start no segment name witnesses -- a trim
175    /// that landed inside a segment, or a tiered floor below every segment
176    /// still on disk -- survives a reopen. Without that, [`Log::open`] derives
177    /// the start from the first surviving base offset and serves records that
178    /// a `DeleteRecords` already deleted. Retention moves the start to a
179    /// segment boundary instead and defers the write; see
180    /// [`Log::checkpoint_log_start_if_due`].
181    ///
182    /// When the start does move up, the rest of the log follows it the way
183    /// Kafka's `maybeIncrementLogStartOffset` makes it follow:
184    ///
185    /// - the leader-epoch cache drops what lies below the new start
186    ///   (`leaderEpochCache.truncateFromStart`, see
187    ///   [`LeaderEpochCheckpoint::truncate_from_start`]);
188    /// - a complete transaction whose marker lies below the new start stops
189    ///   holding the last stable offset
190    ///   (`ProducerStateManager.onLogStartOffsetIncremented` →
191    ///   `removeUnreplicatedTransactions`), because nothing a leader change
192    ///   could truncate is left of it;
193    /// - the first unstable offset is recomputed (`maybeIncrementFirstUnstableOffset`).
194    ///
195    /// `new_start` must be non-negative.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`LogError::InvalidArgument`] if `new_start` is negative, and
200    /// [`LogError::Io`] if the log-start or leader-epoch checkpoint cannot be
201    /// written.
202    pub fn set_log_start_offset(&mut self, new_start: Offset) -> Result<(), LogError> {
203        self.advance_log_start_offset(new_start, StartCheckpoint::Now)
204    }
205
206    /// [`Log::set_log_start_offset`] for retention, which moves the start to
207    /// the base offset of the first segment it keeps. The segment names
208    /// already record that start, so the checkpoint write waits for
209    /// [`Log::checkpoint_log_start_if_due`], as Kafka's waits for
210    /// `log.flush.start.offset.checkpoint.interval.ms`.
211    pub(super) fn follow_retained_segments(&mut self, now: Instant) -> Result<(), LogError> {
212        self.advance_log_start_offset(self.first_local_offset(), StartCheckpoint::Deferred(now))
213    }
214
215    fn advance_log_start_offset(
216        &mut self,
217        new_start: Offset,
218        checkpoint: StartCheckpoint,
219    ) -> Result<(), LogError> {
220        if new_start < 0 {
221            return Err(LogError::InvalidArgument(
222                "set_log_start_offset: new_start must be >= 0".into(),
223            ));
224        }
225        let incremented = new_start > self.start_offset;
226        let new_start = self.start_offset.max(new_start);
227        match checkpoint {
228            // The checkpoint is durable when this returns, directory sync
229            // included: `DeleteRecords` is acknowledged as soon as the trim
230            // does, and a trimmed partition can then sit idle with no later
231            // `sync()` to pay a deferred debt. A file that already holds the
232            // value is durable already.
233            StartCheckpoint::Now if self.checkpointed_start != Some(new_start) => {
234                self.write_start_checkpoint(new_start)?;
235            }
236            StartCheckpoint::Now => {}
237            StartCheckpoint::Deferred(now) => self.defer_start_checkpoint(new_start, now),
238        }
239        self.start_offset = new_start;
240        // Whoever calls this deleted the records below `new_start`, so the
241        // floor now means something a reader may be refused against, and the
242        // checkpoint carries that meaning across a reopen.
243        self.start_offset_established = true;
244        if incremented {
245            self.epoch_checkpoint.truncate_from_start(new_start)?;
246            self.unreplicated
247                .retain(|_, marker_offset| *marker_offset >= new_start);
248            self.refresh_lso()?;
249        }
250        Ok(())
251    }
252
253    /// Whether [`Log::follow_leader_log_start`] would change anything for a
254    /// leader that reports `leader_start`. A follower answers this on every
255    /// fetch response, so it only reads.
256    #[must_use]
257    pub fn differs_from_leader_log_start(&self, leader_start: Offset) -> bool {
258        if self
259            .config
260            .read()
261            .unwrap_or_else(PoisonError::into_inner)
262            .remote_storage_enable
263        {
264            self.established_log_start() != Some(leader_start)
265        } else {
266            leader_start > self.start_offset
267        }
268    }
269
270    /// Take a leader's global log start as this follower's own, and return
271    /// the resulting log start.
272    ///
273    /// A log with no remote tier only ever raises its start, as Kafka's
274    /// `ReplicaFetcherThread.processPartitionData` →
275    /// `maybeIncrementLogStartOffset` does, through [`Log::trim_to_offset`].
276    ///
277    /// A remote-storage-enabled log takes the leader's start in either
278    /// direction and establishes it. The leader is the replica that owns the
279    /// archive and moves its floor, so a follower start above the leader's is
280    /// one no acknowledged deletion put there:
281    ///
282    /// - A broker without the fix for #1444 reported a restarted leader's
283    ///   inferred start, the base of its first local segment, and its
284    ///   followers established it. Such a floor hides the archive below it,
285    ///   and remote retention deletes against it once the follower leads.
286    /// - [`Log::reset_to`] rebuilds a follower at its leader's *local* start,
287    ///   and Kafka's `truncateFullyAndStartAt(localLogStartOffset,
288    ///   leaderLogStartOffset)` gives the rebuilt log the leader's global
289    ///   start beneath it.
290    ///
291    /// Lowering the start leaves the local segments and the leader-epoch
292    /// cache alone, so [`Log::log_start_epoch`] answers `None` until a later
293    /// raise cuts the cache again.
294    ///
295    /// # Errors
296    /// Returns [`LogError::InvalidArgument`] if `leader_start` is negative,
297    /// and [`LogError::Io`] if the log-start checkpoint cannot be written or
298    /// a raise fails.
299    pub fn follow_leader_log_start(&mut self, leader_start: Offset) -> Result<Offset, LogError> {
300        if !self
301            .config
302            .read()
303            .unwrap_or_else(PoisonError::into_inner)
304            .remote_storage_enable
305            || leader_start > self.start_offset
306        {
307            return self.trim_to_offset(leader_start);
308        }
309        if leader_start < 0 {
310            return Err(LogError::InvalidArgument(
311                "follow_leader_log_start: leader_start must be >= 0".into(),
312            ));
313        }
314        self.write_start_checkpoint(leader_start)?;
315        self.start_offset = leader_start;
316        self.start_offset_established = true;
317        Ok(leader_start)
318    }
319
320    /// Move the log start down to `ceiling` when it sits above it. Truncation
321    /// is the one caller: Kafka's `UnifiedLog.truncateTo` sets
322    /// `logStartOffset = Math.min(targetOffset, logStartOffset)`, so a cut that
323    /// lands below a start `DeleteRecords` or retention had advanced pulls the
324    /// start back onto the retained data. The new value is checkpointed for
325    /// the same reason [`Log::set_log_start_offset`] checkpoints an advance.
326    pub(super) fn lower_log_start_offset(&mut self, ceiling: Offset) -> Result<(), LogError> {
327        if ceiling >= self.start_offset {
328            return Ok(());
329        }
330        self.write_start_checkpoint(ceiling)?;
331        self.start_offset = ceiling;
332        Ok(())
333    }
334
335    /// Reset the log to be empty at `new_base`.
336    ///
337    /// This method drops every segment and every on-disk file, then creates
338    /// a fresh active segment at `new_base`. The replicator's
339    /// `OFFSET_OUT_OF_RANGE` recovery path uses it when the follower has
340    /// fallen behind the leader's `log_start`. `truncate_to` cannot help
341    /// there, because `log_start` must move *forward* past the point where
342    /// no local data exists.
343    #[instrument(level = "info", skip_all, fields(new_base = new_base.0), err)]
344    /// # Errors
345    /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
346    pub fn reset_to(&mut self, new_base: Offset) -> Result<(), LogError> {
347        self.rollover_flusher.finish()?;
348        // A read planned before the reset must not serve what it finds now.
349        self.truncations.fetch_add(1, Ordering::SeqCst);
350        self.roll_jitter = None;
351        if new_base < 0 {
352            return Err(LogError::OffsetMismatch {
353                expected: Offset(0),
354                actual: new_base,
355            });
356        }
357
358        producer_snapshot::remove_all(&self.dir)?;
359        self.invalidate_delivery_schedule(new_base);
360
361        // Drop every sealed segment + its on-disk files.
362        while let Some(segment) = self.segments.last() {
363            let base = segment.base_offset();
364            self.remove_truncated_segment_files(base)?;
365            self.segments.pop();
366            self.sealed_txn_indexes.remove(&base);
367            self.stamp_indexes.remove(&base);
368        }
369
370        // Drop the active segment + its on-disk files.
371        if let Some(active) = &self.active {
372            let base = active.base_offset();
373            self.remove_truncated_segment_files(base)?;
374        }
375
376        // A hard reset re-bases the local log after a divergence or a
377        // snapshot install. It says nothing about the remote tier, which may
378        // still hold lower offsets, so the floor moves without becoming a
379        // statement anything may be refused or deleted against -- and the
380        // checkpoint that would restore such a statement goes with it.
381        self.start_offset = new_base;
382        self.start_offset_established = false;
383        log_start_offset_checkpoint::remove(&self.dir)?;
384        self.forget_start_checkpoint();
385
386        let mut new_active = Segment::create(&self.dir, new_base)?;
387        new_active.set_io(Arc::clone(&self.io));
388        self.active_txn_index = TxnIndex::open(new_active.txn_index_path())?;
389        let stamp_index_path = new_active.stamp_index_path();
390        self.clear_producer_and_transaction_state();
391        self.sealed_txn_indexes.clear();
392        self.stamp_indexes.clear();
393        self.lso = new_active.last_offset() + 1; // = new_base (empty segment)
394        self.active = Some(new_active);
395        #[cfg(not(target_os = "wasi"))]
396        self.restart_writeback_pacing(0);
397        self.dir_sync_needed = true;
398        // Preserve any injected stamp source; reopen its (fresh) sidecar.
399        self.reopen_active_stamp_index(new_base, stamp_index_path)?;
400        // The log now holds no records, so the leader-epoch cache must hold no
401        // entries (Kafka's truncateFullyAndStartAt → leaderEpochCache.clearAndFlush).
402        // Leaving stale entries makes a follower advertise a `last_fetched_epoch`
403        // it has no record for, so the leader's KIP-320 reconciliation serves a
404        // batch at a mismatched base offset and the follower loops forever on
405        // append_at — a phantom ISR member that pins the high-watermark.
406        self.epoch_checkpoint.clear()?;
407        // Kafka's `truncateFullyAndStartAt` rebuilds the (now empty) producer
408        // state and takes a snapshot at the new base: its
409        // `ProducerStateManager.truncateFullyAndStartAt` resets the last
410        // snapshot offset to 0, so `takeSnapshot` writes one at any base above
411        // 0.
412        if new_base.0 > 0 {
413            producer_snapshot::write(&*self.io, &self.dir, new_base, &self.producer_state)?;
414        }
415        self.reap_deleted_files(SystemTime::now())
416    }
417
418    /// Kafka's `ProducerStateManager.takeSnapshot`: write the producer state at
419    /// the log end offset, so a reopen restores it from the snapshot and
420    /// replays nothing. A snapshot already at that offset is kept, and none is
421    /// written at or below the log start, which Kafka takes as the last
422    /// snapshot offset when no snapshot survives a reload.
423    ///
424    /// # Errors
425    /// Returns an error when the snapshot cannot be written.
426    pub fn take_producer_snapshot(&mut self) -> Result<(), LogError> {
427        let log_end = self.log_end_offset();
428        if log_end.0 > self.producer_reload_range(log_end).log_start {
429            self.sync()?;
430            producer_snapshot::write(&*self.io, &self.dir, log_end, &self.producer_state)?;
431        }
432        Ok(())
433    }
434
435    /// Refuses every append from now on with [`LogError::Fenced`], and stops
436    /// any compaction job.
437    ///
438    /// The broker fences a partition's log, under the log's mutex, before it
439    /// removes the partition's directory. An append that holds the mutex
440    /// first finishes before the directory goes; any later one, from whatever
441    /// task, is refused rather than written into a log that is about to be
442    /// deleted. Reads are unaffected.
443    ///
444    /// A compaction job builds without the mutex, writing `.cleaned` files
445    /// into the directory. The fence stops it at its next batch and returns
446    /// only once it writes nothing more, so the directory can go as soon as
447    /// the fence returns. [`Log::finish_compaction`] then installs nothing.
448    pub fn fence(&mut self) {
449        self.fenced = true;
450        self.compaction_gate.fence();
451    }
452
453    pub(super) fn ensure_unfenced(&self) -> Result<(), LogError> {
454        if self.fenced {
455            Err(LogError::Fenced)
456        } else {
457            Ok(())
458        }
459    }
460
461    /// Whether a raw read from `fetch_offset` may need bytes outside memory.
462    ///
463    /// Buffered reads of this process's active-segment appends use the page cache. Direct reads can run in memory when the tail cache covers the sparse-index floor and every byte after it. Older segments, reopened bytes, and evicted direct tails need the blocking path. The caller must retain the log lock through the read so a concurrent append cannot evict the checked bytes.
464    #[must_use]
465    pub fn read_may_block(&self, fetch_offset: Offset) -> bool {
466        self.active.as_ref().is_none_or(|active| {
467            fetch_offset < active.base_offset()
468                || active.read_may_block(fetch_offset, self.opened_end)
469        })
470    }
471
472    /// Whether appending `incoming_size` bytes whose newest record is stamped
473    /// `incoming_max_timestamp` may wait on more than the page cache.
474    ///
475    /// It is true when the log's flush policy can make an append fsync
476    /// (`flush.messages`, `flush.ms`, or a flush on every append), and when
477    /// the append would roll the active segment, which creates files and can
478    /// wait for an earlier roll's flush budget, and for every append to a log
479    /// with `preallocate`: that append can reserve the segment's blocks, open
480    /// its `O_DIRECT` handle, or wait for room in its direct write queue. It
481    /// errs towards true: a caller that sees false may run the append where a
482    /// wait would stall others.
483    pub fn append_may_block(
484        &mut self,
485        incoming_size: ByteSize,
486        incoming_max_timestamp: i64,
487    ) -> bool {
488        self.append_may_block_with(incoming_size, incoming_max_timestamp, None)
489    }
490
491    /// Whether appending this one verbatim batch can wait on storage. An initialized direct writer with queue space can accept ordinary batches without a worker handoff. Flushes, epoch changes, segment setup, and queue backpressure still require blocking placement. The caller must hold the log lock through the append.
492    pub fn verbatim_append_may_block(&mut self, batch: &VerbatimBatch) -> bool {
493        self.append_may_block_with(
494            ByteSize::from_bytes(u64::try_from(batch.bytes.len()).unwrap_or(u64::MAX)),
495            batch.max_timestamp,
496            Some(batch),
497        )
498    }
499
500    fn can_queue_verbatim(&self, batch: &VerbatimBatch, config: &LogConfig) -> bool {
501        !batch.is_transactional
502            && batch.bytes.len() >= HEADER_LEN
503            && batch
504                .bytes
505                .get(CRC_COVERAGE_START..)
506                .and_then(<[u8]>::first_chunk::<2>)
507                .is_some_and(|&attributes| {
508                    !Attributes(i16::from_be_bytes(attributes)).is_control_batch()
509                })
510            && config.delivery_policy == DeliveryPolicy::Immediate
511            && config.message_timestamp_type == TimestampType::CreateTime
512            && self.active.as_ref().is_some_and(|active| {
513                active.can_queue_append(batch.bytes.len(), config.segment_size)
514            })
515    }
516
517    fn verbatim_epoch_is_current(&self, batch: &VerbatimBatch) -> bool {
518        !batch.leader_epoch.is_known()
519            || self.epoch_checkpoint.entries().last().is_some_and(|entry| {
520                entry.epoch == batch.leader_epoch
521                    // A follower can clear a reconciled frontier before appending. The local log end is the lower bound shared by both append paths.
522                    && entry.start_offset <= self.log_end_offset()
523            })
524    }
525
526    fn append_may_block_with(
527        &mut self,
528        incoming_size: ByteSize,
529        incoming_max_timestamp: i64,
530        verbatim: Option<&VerbatimBatch>,
531    ) -> bool {
532        let (flushes, segment_size, roll_interval, index_size) = {
533            let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
534            let storage_may_block = match config.segment_allocation {
535                SegmentAllocation::Preallocate => {
536                    verbatim.is_none_or(|batch| !self.can_queue_verbatim(batch, &config))
537                }
538                SegmentAllocation::OnWrite => {
539                    self.active.as_ref().is_some_and(Segment::writes_direct)
540                }
541            };
542            (
543                config.flush_on_append
544                    || config.flush_messages.is_some()
545                    || config.flush_interval.is_some()
546                    || self.stamp_source.is_some()
547                    || storage_may_block
548                    || verbatim.is_some_and(|batch| !self.verbatim_epoch_is_current(batch))
549                    || (verbatim.is_some()
550                        && config.message_timestamp_type == TimestampType::LogAppendTime),
551                config.segment_size,
552                config.segment_roll_interval,
553                config.segment_index_size,
554            )
555        };
556        if flushes {
557            return true;
558        }
559        let roll_interval = self.jittered_roll_interval(roll_interval);
560        self.should_roll_for_incoming(
561            incoming_size,
562            incoming_max_timestamp,
563            segment_size,
564            roll_interval,
565            index_size,
566        )
567    }
568
569    /// Next offset that `append` will assign.
570    #[must_use]
571    pub fn log_end_offset(&self) -> Offset {
572        if let Some(active) = &self.active {
573            return active.last_offset() + 1;
574        }
575        Offset(0)
576    }
577
578    /// Total `.log` size across sealed and active segments.
579    ///
580    /// The value comes from the segments' tracked logical size, not from a
581    /// filesystem stat. It therefore shows buffered appends immediately and
582    /// in the same way on every platform. On some operating systems a
583    /// directory stat can lag an open, unflushed write handle.
584    #[must_use]
585    pub fn size(&self) -> ByteSize {
586        let active = self.active.as_ref().map_or(ByteSize::ZERO, Segment::size);
587        self.segments
588            .iter()
589            .fold(active, |total, seg| total + seg.size())
590    }
591
592    /// First unstable offset: the first offset of the earliest transaction
593    /// that is open, or complete with a marker the high watermark has not
594    /// passed. It is the log end offset when there is no such transaction.
595    ///
596    /// This value does not know the high watermark. A reader must use
597    /// [`Log::last_stable_offset`], which releases replicated transactions
598    /// and caps the answer at the high watermark.
599    #[must_use]
600    pub fn lso(&self) -> Offset { self.lso }
601
602    /// First offset of `producer_id`'s currently open transaction on this
603    /// partition, or `None` when no transaction from that producer is pending.
604    #[must_use]
605    pub fn pending_transaction_start(&self, producer_id: ProducerId) -> Option<Offset> {
606        self.pending.get(producer_id).copied()
607    }
608
609    /// Producer and coordinator generations used to admit one transaction
610    /// marker, plus whether that producer currently has an open transaction.
611    /// Missing generations use Kafka's `-1` sentinel.
612    #[must_use]
613    pub fn transaction_marker_state(&self, producer_id: ProducerId) -> (i16, i32, bool) {
614        let producer_epoch = self
615            .producer_state
616            .get(&producer_id)
617            .map_or(-1, |entry| entry.producer_epoch);
618        let coordinator_epoch = self
619            .coordinator_epochs
620            .get(&producer_id)
621            .copied()
622            .unwrap_or(-1);
623        (
624            producer_epoch,
625            coordinator_epoch,
626            self.pending.contains_key(producer_id),
627        )
628    }
629
630    /// The producer state this log holds for `producer_id`, if any.
631    ///
632    /// Append and recovery update the same entry with the same rule, so the
633    /// value after a live append equals the value that a reopen rebuilds.
634    #[must_use]
635    pub fn producer_state_entry(&self, producer_id: ProducerId) -> Option<ProducerSnapshotEntry> {
636        self.producer_state.get(&producer_id).copied()
637    }
638
639    /// Producer state restored from the newest valid Kafka-compatible
640    /// snapshot and the uncovered local log tail.
641    #[must_use]
642    pub fn producer_state_snapshot(&self) -> Vec<ProducerSnapshotEntry> {
643        self.producer_state.values().copied().collect()
644    }
645
646    /// Every producer's state with the batches it appended before its last
647    /// one: [`Self::producer_state_snapshot`] plus the retained batches that
648    /// Kafka's `ProducerStateEntry` keeps for duplicate detection. After a
649    /// reopen they are the batches the replay past the loaded snapshot
650    /// rebuilt.
651    #[must_use]
652    pub fn recovered_producers(&self) -> Vec<RecoveredProducer> {
653        self.producer_state
654            .values()
655            .map(|entry| RecoveredProducer {
656                entry: *entry,
657                earlier: self
658                    .earlier_batches
659                    .get(&entry.producer_id)
660                    .map_or_default(|earlier| earlier.iter().copied().collect()),
661            })
662            .collect()
663    }
664
665    /// Close all segments, first taking a producer-state snapshot at the log
666    /// end as Kafka's `UnifiedLog.close` does. A snapshot that cannot be
667    /// written is not an error: the next open replays the log instead.
668    ///
669    /// A running compaction build stops first, as on [`Log::fence`], and the
670    /// call returns once it writes nothing more.
671    pub fn close(mut self) {
672        // A compaction build writes into the directory without the log; stop
673        // it, as the fence does, before the caller moves or deletes the
674        // directory.
675        self.compaction_gate.fence();
676        if let Err(error) = self.take_producer_snapshot() {
677            tracing::warn!(
678                dir = %self.dir.display(),
679                %error,
680                "producer-state snapshot on close failed; the next open replays the log"
681            );
682        }
683        drop(self);
684    }
685
686    /// Atomically swap the active `LogConfig`.
687    ///
688    /// The next retention or roll check reads the new value. In-flight
689    /// `append` calls hold the lock for very short windows and will not see
690    /// a half-applied config.
691    ///
692    /// Callers can use this method through `&self`. The `Arc<RwLock<…>>`
693    /// wrapper permits mutation of the inner value without an exclusive
694    /// borrow on the `Log`.
695    ///
696    /// Delivery settings apply to subsequent watermark advances. Changing
697    /// clock uncertainty does not retract the already published delivery
698    /// prefix; see [`LogConfig::delivery_clock_uncertainty`].
699    pub fn set_config(&self, new: LogConfig) {
700        *self.config.write().unwrap_or_else(PoisonError::into_inner) = new;
701    }
702
703    /// Snapshot the current config. This allocates a clone, which is cheap
704    /// because `LogConfig` is small and `Clone`.
705    #[must_use]
706    pub fn config_snapshot(&self) -> LogConfig {
707        self.config
708            .read()
709            .unwrap_or_else(PoisonError::into_inner)
710            .clone()
711    }
712
713    /// Replace the log's I/O implementation for fault-injection tests.
714    ///
715    /// This reaches every durable write the log makes: the active `.log`, both
716    /// sparse indexes of every segment, the `.stampindex` sidecars, the
717    /// leader-epoch checkpoint, and -- through `self.io`, which the free
718    /// functions take -- the producer snapshots, the compaction swap, and the
719    /// segment deletions retention performs.
720    #[cfg(any(test, feature = "test-helpers"))]
721    pub fn test_set_io(&mut self, io: Arc<dyn LogIo>) {
722        self.io = Arc::clone(&io);
723        for segment in &mut self.segments {
724            segment.set_io(Arc::clone(&io));
725        }
726        if let Some(active) = &mut self.active {
727            active.set_io(Arc::clone(&io));
728        }
729        for index in self.stamp_indexes.values_mut() {
730            index.set_io(Arc::clone(&io));
731        }
732        self.epoch_checkpoint.set_io(io);
733    }
734
735    /// Return all aborted transactions whose offset range overlaps
736    /// `[start, end)`, including entries in sealed segments.
737    ///
738    /// Kafka's `UnifiedLog.collectAbortedTransactions`: the search starts at
739    /// the first segment that reaches `start` and stops at the first index
740    /// that reports it complete (see [`TxnIndex::collect_aborted`]).
741    #[must_use]
742    pub fn aborted_in_range(&self, start: Offset, end: Offset) -> Vec<AbortedTxn> {
743        let mut aborted = Vec::new();
744        // Segment last offsets rise with their base offsets.
745        let first = self
746            .segments
747            .partition_point(|segment| segment.last_offset() < start);
748        let sealed = self
749            .segments
750            .get(first)
751            .map(Segment::base_offset)
752            .into_iter()
753            .flat_map(|first_base| {
754                self.sealed_txn_indexes
755                    .range(first_base..)
756                    .map(|(_, index)| index)
757            });
758        for index in sealed.chain(iter::once(&self.active_txn_index)) {
759            if index.collect_aborted(start, end, &mut aborted) {
760                break;
761            }
762        }
763        aborted
764    }
765
766    /// Access the per-partition leader-epoch checkpoint.
767    #[must_use]
768    pub fn epoch_checkpoint(&self) -> &LeaderEpochCheckpoint { &self.epoch_checkpoint }
769
770    /// Kafka's `UnifiedLog.assignEpochStartOffset`: record that `epoch`
771    /// starts at `start_offset` through
772    /// [`LeaderEpochCheckpoint::assign`]. `Partition.makeLeader` calls it with
773    /// the log end offset when a replica takes a new leader epoch, so the
774    /// leader can place a follower's `last_fetched_epoch` before it has
775    /// written anything in that epoch.
776    ///
777    /// # Errors
778    /// Returns an error for a negative epoch or offset, or when the
779    /// checkpoint cannot be persisted.
780    pub fn assign_epoch_start_offset(
781        &mut self,
782        epoch: krabka_ids::LeaderEpoch,
783        start_offset: Offset,
784    ) -> Result<(), LogError> {
785        self.epoch_checkpoint.assign(epoch, start_offset)
786    }
787
788    /// Reconcile append-at offset assignment to an external next-offset frontier.
789    ///
790    /// Diskless partitions use the `KRaft` metadata log as the offset authority.
791    /// After a crash, `KRaft` may have committed a next-offset that is ahead of the
792    /// recovered local WAL tail. In that case the gap is intentional: the caller
793    /// sets this frontier and the next append-at must use it instead of the local
794    /// LEO. Classic logs never call this method and keep the default frontier 0.
795    pub fn reconcile_next_offset(&mut self, frontier: Offset) {
796        self.reconciled_frontier = self.reconciled_frontier.max(frontier);
797    }
798
799    /// Drop the frontier [`Log::reconcile_next_offset`] set, so append-at
800    /// bases are held only to the log end offset again.
801    ///
802    /// The frontier binds the broker that assigns this log's offsets. A
803    /// follower assigns none: it appends its leader's batches at the leader's
804    /// offsets, and Kafka's `UnifiedLog.appendAsFollower` refuses only a batch
805    /// below the log end. A diskless replica that reopened behind the
806    /// committed frontier -- it was down while its leader appended -- must
807    /// take the batches between its log end and that frontier from the
808    /// leader, and the frontier would refuse every one of them.
809    pub fn clear_reconciled_next_offset(&mut self) { self.reconciled_frontier = Offset(0); }
810}
811
812#[cfg(test)]
813mod tests {
814    use {
815        super::*,
816        crate::{
817            log::test_support::{
818                TempLog, append_samples, append_transaction, rolled_sample_log, sample_batch,
819                sample_batch_with_epoch, test_log, tiny_segments,
820            },
821            test_support::open_log,
822        },
823        assert2::check,
824        krabka_ids::LeaderEpoch,
825        krabka_units::prelude::{kibibytes, minutes},
826        std::path::Path,
827        tempfile::tempdir,
828    };
829
830    /// A hard reset leaves the log empty at the new base, with the last stable
831    /// offset there too.
832    ///
833    /// `lso` is derived from the fresh segment rather than from the base it was
834    /// asked for, and it gates what a `read_committed` consumer may see -- one
835    /// short of the base would expose an offset the log does not have.
836    #[test]
837    fn a_reset_puts_the_stable_offset_at_the_new_base() {
838        let TempLog { dir: _dir, mut log } = test_log();
839        append_samples(&mut log, 3, 2);
840        check!(log.log_end_offset() == Offset(6));
841
842        log.reset_to(Offset(50)).expect("reset");
843        check!(
844            log.log_start_offset() == Offset(50),
845            "starts at the new base"
846        );
847        check!(log.log_end_offset() == Offset(50), "and is empty there");
848        check!(
849            log.lso() == Offset(50),
850            "the stable offset is the base, got {:?}",
851            log.lso()
852        );
853    }
854
855    /// Zero is a legal log start; only a negative one is rejected.
856    #[test]
857    fn the_log_start_may_be_set_to_zero_but_not_below() {
858        let TempLog { dir: _dir, mut log } = test_log();
859        check!(
860            log.set_log_start_offset(Offset(0)).is_ok(),
861            "zero is a real offset"
862        );
863        check!(log.set_log_start_offset(Offset(7)).is_ok());
864        check!(
865            log.set_log_start_offset(Offset(-1)).is_err(),
866            "negative is not"
867        );
868    }
869
870    /// Kafka's `maybeIncrementLogStartOffset` → `onLogStartOffsetIncremented`:
871    /// a complete transaction whose marker falls below the new log start
872    /// stops holding the last stable offset, and one whose marker the start
873    /// only reaches keeps holding it. Producer 42 writes a transaction at
874    /// offset 0 and commits it at offset 1, the high watermark never passes
875    /// the marker, and three plain records follow at offsets 2 to 4.
876    #[yare::parameterized(
877        reaches_marker = { "the start reaches the marker", 1, false },
878        passes_marker = { "the start passes the marker", 2, true },
879    )]
880    fn raising_the_log_start_past_a_marker_releases_its_transaction(
881        name: &'static str,
882        new_start: i64,
883        released: bool,
884    ) {
885        use crate::log::test_support::commit_marker;
886
887        let TempLog { dir: _dir, mut log } = test_log();
888        append_transaction(&mut log, 42, 0, &["a"]);
889        log.append(&mut commit_marker(42, 0)).unwrap();
890        log.append(&mut sample_batch(3)).unwrap();
891        check!(log.lso() == Offset(0), "{name}: held before the move");
892
893        log.set_log_start_offset(Offset(new_start)).unwrap();
894
895        check!(
896            (log.lso() == log.log_end_offset()) == released,
897            "{name}: lso {:?}",
898            log.lso()
899        );
900    }
901
902    /// Kafka's `maybeIncrementLogStartOffset` →
903    /// `leaderEpochCache.truncateFromStart`: raising the start drops the
904    /// epochs that end below it, the epoch that covers it starts there, and
905    /// the trimmed history is what a reopen reads back. A start that does not
906    /// move leaves the cache alone.
907    #[test]
908    fn raising_the_log_start_truncates_the_epoch_cache_from_the_start() {
909        use crate::leader_epoch_checkpoint::EpochEntry;
910
911        let TempLog { dir, mut log } = test_log();
912        log.append(&mut sample_batch_with_epoch(3, 1)).unwrap(); // epoch 1 @ 0
913        log.append(&mut sample_batch_with_epoch(3, 2)).unwrap(); // epoch 2 @ 3
914        log.append(&mut sample_batch_with_epoch(3, 4)).unwrap(); // epoch 4 @ 6
915
916        log.set_log_start_offset(Offset(4)).unwrap();
917        log.set_log_start_offset(Offset(2)).unwrap();
918
919        let expected = [
920            EpochEntry {
921                epoch: LeaderEpoch(2),
922                start_offset: Offset(4),
923            },
924            EpochEntry {
925                epoch: LeaderEpoch(4),
926                start_offset: Offset(6),
927            },
928        ];
929        check!(log.epoch_checkpoint().entries() == &expected[..]);
930        drop(log);
931        let reopened = open_log(dir.path());
932        check!(reopened.epoch_checkpoint().entries() == &expected[..]);
933    }
934
935    /// The epoch the remote tier's leader-epoch-cache cleanup may measure
936    /// against is the earliest cache entry an advance of the log start put
937    /// there (#1200). The segments a tiered partition still holds locally
938    /// are not that: evicting them after the archive took them, and a restart
939    /// on what is left, leave the whole epoch history in place and answer
940    /// `None`, so the archive's older epochs are never mistaken for a
941    /// truncated lineage. A `DeleteRecords`-style advance is one, and it is
942    /// still one after a restart.
943    #[test]
944    fn the_log_start_epoch_comes_only_from_an_established_log_start() {
945        let config = LogConfig {
946            remote_storage_enable: true,
947            ..tiny_segments()
948        };
949        // Three batches of three records, each rolling into its own segment:
950        // epoch 1 at 0, epoch 2 at 3, epoch 4 at 6.
951        let filled = |dir: &Path| {
952            let mut log = Log::open(dir, config.clone()).unwrap();
953            for epoch in [1, 2, 4] {
954                log.append(&mut sample_batch_with_epoch(3, epoch)).unwrap();
955            }
956            log
957        };
958
959        // Local retention drops the segments the archive holds, and the
960        // restart infers a log start from what is left.
961        let evicted = tempdir().unwrap();
962        let mut log = filled(evicted.path());
963        check!(log.log_start_epoch() == None, "nobody moved the log start");
964        check!(log.delete_local_segments_through(Offset(6)).unwrap() == 2);
965        drop(log);
966        let log = Log::open(evicted.path(), config.clone()).unwrap();
967        check!(log.epoch_checkpoint().entries().len() == 3);
968        check!(
969            log.log_start_epoch() == None,
970            "the restart inferred a start from the surviving segments"
971        );
972
973        // A start that was moved cuts the cache to the epoch that owns it, and
974        // the restart keeps both.
975        let moved = tempdir().unwrap();
976        let mut log = filled(moved.path());
977        log.set_log_start_offset(Offset(4)).unwrap();
978        check!(log.log_start_epoch() == Some(LeaderEpoch(2)));
979        drop(log);
980        let log = Log::open(moved.path(), config).unwrap();
981        check!(log.log_start_epoch() == Some(LeaderEpoch(2)));
982    }
983
984    /// A log in its own directory holding three segments, at offsets 0, 3
985    /// and 6 and in epochs 1, 2 and 4, and the configuration that opens it
986    /// again.
987    struct ThreeSegments {
988        dir: tempfile::TempDir,
989        log: Log,
990        config: LogConfig,
991    }
992
993    fn three_segments(remote_storage_enable: bool) -> ThreeSegments {
994        let config = LogConfig {
995            remote_storage_enable,
996            ..tiny_segments()
997        };
998        let dir = tempdir().unwrap();
999        let mut log = Log::open(dir.path(), config.clone()).unwrap();
1000        for epoch in [1, 2, 4] {
1001            log.append(&mut sample_batch_with_epoch(3, epoch)).unwrap();
1002        }
1003        ThreeSegments { dir, log, config }
1004    }
1005
1006    /// A leader reports Kafka's `logStartOffset` to its followers, and they
1007    /// raise their own start to it. A remote-storage-enabled log reopened over
1008    /// the segments local retention left infers a start above the whole
1009    /// archive, and Kafka's `LogLoader.load` never takes that one: it reports
1010    /// the checkpointed start, 0 when nobody moved it. A start somebody moved
1011    /// is reported as it is, and a log with no remote tier reports the start
1012    /// its segments witness.
1013    ///
1014    /// Each case fills three segments (offsets 0, 3 and 6), optionally moves
1015    /// the start, drops the two sealed segments from disk, and reopens.
1016    #[yare::parameterized(
1017        tiered_nobody_moved_it = { true, None, 6, 0 },
1018        tiered_moved = { true, Some(4), 4, 4 },
1019        local_only = { false, None, 6, 6 },
1020    )]
1021    fn a_reopened_log_reports_the_start_kafka_reports(
1022        remote_storage_enable: bool,
1023        moved_to: Option<i64>,
1024        log_start: i64,
1025        reported: i64,
1026    ) {
1027        let ThreeSegments {
1028            dir,
1029            mut log,
1030            config,
1031        } = three_segments(remote_storage_enable);
1032        if let Some(start) = moved_to {
1033            log.set_log_start_offset(Offset(start)).unwrap();
1034        }
1035        check!(log.delete_local_segments_through(Offset(6)).unwrap() == 2);
1036        drop(log);
1037
1038        let log = Log::open(dir.path(), config).unwrap();
1039        check!(
1040            (log.log_start_offset(), log.reported_log_start())
1041                == (Offset(log_start), Offset(reported))
1042        );
1043    }
1044
1045    /// How a follower's log came by its start before the leader reports one.
1046    #[derive(Clone, Copy, Debug)]
1047    enum Prior {
1048        /// Nobody moved it.
1049        Untouched,
1050        /// Somebody established it at this offset.
1051        Moved(i64),
1052        /// `reset_to` rebuilt the log at this offset, the leader's local
1053        /// start.
1054        Rebuilt(i64),
1055    }
1056
1057    /// A follower takes its leader's log start. A log with no remote tier only
1058    /// raises its start, as Kafka's `maybeIncrementLogStartOffset` does. A
1059    /// remote-storage-enabled log takes the leader's start in either direction
1060    /// and establishes it: that repairs a floor no deletion put there (a
1061    /// restarted leader's inferred start, which an older broker reported) and
1062    /// gives a rebuilt follower the leader's global start below its own, as
1063    /// Kafka's `truncateFullyAndStartAt` does. The start a follower takes
1064    /// survives a reopen.
1065    ///
1066    /// Each case fills three segments (offsets 0, 3 and 6) before `prior`.
1067    #[yare::parameterized(
1068        tiered_bogus_floor_is_repaired = { true, Prior::Moved(6), 0, true, 0 },
1069        tiered_rebuilt_takes_the_global_start = { true, Prior::Rebuilt(6), 2, true, 2 },
1070        tiered_raise = { true, Prior::Untouched, 4, true, 4 },
1071        tiered_agreeing_floor_is_left_alone = { true, Prior::Moved(4), 4, false, 4 },
1072        local_never_lowers = { false, Prior::Moved(4), 2, false, 4 },
1073        local_raise = { false, Prior::Untouched, 4, true, 4 },
1074    )]
1075    fn a_follower_takes_its_leaders_log_start(
1076        remote_storage_enable: bool,
1077        prior: Prior,
1078        leader_start: i64,
1079        differs: bool,
1080        followed: i64,
1081    ) {
1082        let ThreeSegments {
1083            dir,
1084            mut log,
1085            config,
1086        } = three_segments(remote_storage_enable);
1087        match prior {
1088            Prior::Untouched => {}
1089            Prior::Moved(start) => log.set_log_start_offset(Offset(start)).unwrap(),
1090            Prior::Rebuilt(base) => log.reset_to(Offset(base)).unwrap(),
1091        }
1092
1093        let leader_start = Offset(leader_start);
1094        check!(log.differs_from_leader_log_start(leader_start) == differs);
1095        let result = log.follow_leader_log_start(leader_start).unwrap();
1096        let followed = Offset(followed);
1097        check!(
1098            (result, log.established_log_start()) == (followed, Some(followed)),
1099            "{prior:?}"
1100        );
1101        check!(!log.differs_from_leader_log_start(leader_start));
1102        drop(log);
1103        let reopened = Log::open(dir.path(), config).unwrap();
1104        check!(reopened.log_start_offset() == followed, "{prior:?}");
1105    }
1106
1107    /// A leader start below zero names no offset, and following it is refused
1108    /// on a log with a remote tier as on one without.
1109    #[yare::parameterized(tiered = { true }, local_only = { false })]
1110    fn a_negative_leader_start_is_refused(remote_storage_enable: bool) {
1111        let ThreeSegments {
1112            dir: _dir, mut log, ..
1113        } = three_segments(remote_storage_enable);
1114        check!(matches!(
1115            log.follow_leader_log_start(Offset(-1)),
1116            Err(LogError::InvalidArgument(_))
1117        ));
1118        check!(log.log_start_offset() == Offset(0));
1119    }
1120
1121    /// The log's size is every segment's size added up, the sealed ones as
1122    /// well as the active one.
1123    #[test]
1124    fn log_size_sums_the_sealed_segments_and_the_active_one() {
1125        let dir = tempdir().unwrap();
1126        let log = rolled_sample_log(dir.path());
1127
1128        let expected = log.segments.iter().map(Segment::size).fold(
1129            log.active.as_ref().map_or(ByteSize::ZERO, Segment::size),
1130            |a, b| a + b,
1131        );
1132        check!(
1133            log.size() == expected,
1134            "size {:?}, expected {:?}",
1135            log.size(),
1136            expected
1137        );
1138        check!(
1139            log.size() > kibibytes(1),
1140            "several segments should exceed one"
1141        );
1142    }
1143
1144    #[test]
1145    fn dir_returns_open_path() {
1146        // The broker's KIP-113 move machinery reads this back to
1147        // determine a partition's current owning `log.dir` without
1148        // re-implementing the directory-layout convention.
1149        let TempLog { dir, log } = test_log();
1150        assert2::assert!(log.dir() == dir.path());
1151    }
1152
1153    #[test]
1154    fn reset_to_clears_leader_epoch_checkpoint() {
1155        let TempLog { dir, mut log } = test_log();
1156        // A follower that replicated real data builds an epoch history.
1157        log.append(&mut sample_batch_with_epoch(3, 1)).unwrap(); // epoch 1 @ 0
1158        log.append(&mut sample_batch_with_epoch(2, 2)).unwrap(); // epoch 2 @ 3
1159        log.append(&mut sample_batch_with_epoch(1, 5)).unwrap(); // epoch 5 @ 5
1160        assert2::assert!(log.epoch_checkpoint().latest_epoch() == Some(LeaderEpoch(5)));
1161
1162        // Hard reset to an empty log — the replicator's OFFSET_OUT_OF_RANGE
1163        // recovery path (Kafka's `truncateFullyAndStartAt`). The log now has
1164        // NO records, so it must advertise NO leader epoch. Otherwise the
1165        // follower keeps sending a stale `last_fetched_epoch` and the leader's
1166        // KIP-320 reconciliation serves a batch at a mismatched base offset,
1167        // looping forever on `append_at` (phantom ISR member → pinned HW →
1168        // acks=all stall).
1169        log.reset_to(Offset(0)).unwrap();
1170
1171        assert2::assert!(log.epoch_checkpoint().latest_epoch() == None);
1172        assert2::assert!(log.epoch_checkpoint().entries() == &[][..]);
1173        // The cleared state must survive a reopen (a restarted broker re-reads
1174        // the on-disk checkpoint file).
1175        let reopened = open_log(dir.path());
1176        assert2::assert!(reopened.epoch_checkpoint().entries().is_empty());
1177    }
1178
1179    #[test]
1180    fn reset_to_nonzero_base_clears_all_epochs_not_just_tail() {
1181        // Guards against the subtly-wrong fix `truncate_from_end(new_base)`,
1182        // which retains an entry whose `start_offset < new_base` even though
1183        // the reset log holds no records below `new_base`.
1184        let TempLog { dir: _dir, mut log } = test_log();
1185        log.append(&mut sample_batch_with_epoch(3, 1)).unwrap(); // epoch 1 @ 0
1186        assert2::assert!(log.epoch_checkpoint().latest_epoch() == Some(LeaderEpoch(1)));
1187        log.reset_to(Offset(1000)).unwrap(); // empty log starting at 1000
1188        assert2::assert!(log.epoch_checkpoint().entries().is_empty());
1189    }
1190
1191    #[test]
1192    fn set_config_swaps_active_config() {
1193        let dir = tempdir().expect("tempdir");
1194        let log = Log::open(dir.path(), LogConfig {
1195            retention: Some(minutes(1)),
1196            ..LogConfig::default()
1197        })
1198        .expect("open");
1199        log.set_config(LogConfig {
1200            retention: Some(minutes(2)),
1201            ..LogConfig::default()
1202        });
1203        assert2::assert!(log.config_snapshot().retention == Some(minutes(2)));
1204    }
1205
1206    #[test]
1207    fn transaction_marker_state_and_producer_state_entry() {
1208        use crate::log::test_support::commit_marker;
1209
1210        let TempLog { dir: _dir, mut log } = test_log();
1211        let pid = ProducerId(42);
1212        assert2::assert!(log.transaction_marker_state(pid) == (-1, -1, false));
1213        assert2::assert!(log.producer_state_entry(pid).is_none());
1214
1215        let mut batch = sample_batch(1);
1216        batch.producer_id = 42;
1217        batch.producer_epoch = 2;
1218        batch.base_sequence = 0;
1219        batch.attributes = batch.attributes.with_transactional(true);
1220        log.append(&mut batch).unwrap();
1221
1222        assert2::assert!(log.transaction_marker_state(pid) == (2, -1, true));
1223        let entry = log
1224            .producer_state_entry(pid)
1225            .expect("producer state exists");
1226        assert2::assert!(entry.producer_id == 42);
1227        assert2::assert!(entry.producer_epoch == 2);
1228
1229        let mut marker = commit_marker(42, 2);
1230        log.append(&mut marker).unwrap();
1231        assert2::assert!(log.transaction_marker_state(pid) == (2, 17, false));
1232
1233        log.close();
1234    }
1235}