Skip to main content

krabka_log/
log.rs

1//! `Log`: a sorted collection of `Segment`s with append, read, and truncate.
2//!
3//! This file holds the `Log` type itself. Every operation over it lives in a
4//! submodule named for the concern it covers, and each of those submodules
5//! adds its own `impl Log` block: [`self::open`] for opening a directory and
6//! rebuilding recovered state, [`self::append`] and [`self::verbatim`] for the
7//! two write paths, [`self::read`] for the three read paths, and so on. The
8//! private fields below stay in this module so that every one of those
9//! submodules, as a descendant, can reach them.
10
11use {
12    crate::{
13        config::LogConfig,
14        error::LogError,
15        io::LogIo,
16        leader_epoch_checkpoint::LeaderEpochCheckpoint,
17        producer_snapshot::{ProducerBatchMetadata, ProducerSnapshotEntry},
18        segment::Segment,
19        stamp_index::{OffsetRange, StampIndex},
20        stamp_source::StampSource,
21        txn_index::TxnIndex,
22    },
23    krabka_ids::{Offset, ProducerId},
24    std::{
25        collections::{BTreeMap, HashMap, VecDeque},
26        path::PathBuf,
27        sync::{Arc, RwLock, atomic::AtomicU64},
28        time::{Instant, SystemTime},
29    },
30};
31
32mod active_producers;
33mod append;
34mod compaction;
35pub mod control;
36mod delivery;
37#[cfg(test)]
38#[cfg(unix)]
39mod direct_writes;
40#[cfg(test)]
41mod faults;
42mod maintenance;
43mod open;
44#[cfg(not(target_os = "wasi"))]
45mod page_cache;
46mod pending_transactions;
47#[cfg(test)]
48mod preallocation;
49mod read;
50mod rollover_flush;
51mod stamp;
52mod start_checkpoint;
53mod state;
54mod sync;
55#[cfg(test)]
56mod test_support;
57mod tick;
58mod tiering;
59mod timestamp;
60mod transaction;
61mod truncate;
62mod verbatim;
63mod verbatim_group;
64mod verification;
65
66#[krabka_macros::sendfile_cfg]
67pub use self::read::RawReadDesc;
68pub use self::{
69    active_producers::ActiveProducer,
70    compaction::{BuiltCompaction, CompactionContext, CompactionJob, CompactionOutcome},
71    control::BARRIER_CONTROL_TYPE,
72    maintenance::{DueDeletions, FailedDeletion},
73    read::{RawRead, RawReadPlan, ReadOutput},
74    tiering::{ActiveSegmentExport, SegmentExport},
75    verbatim::VerbatimBatch,
76    verification::{TransactionAppendRefusal, TransactionalBatch, VerificationGuard},
77};
78
79/// The jitter sample that shortens the roll interval of one active segment.
80#[derive(Debug, Clone, Copy)]
81struct RollJitter {
82    /// The base offset of the active segment the sample was drawn for.
83    segment_base: Offset,
84    sample: u64,
85}
86
87/// Segment files that retention renamed away and has yet to delete.
88#[derive(Debug, Clone)]
89struct PendingDelete {
90    /// The instant from which the files may be deleted.
91    deadline: SystemTime,
92    paths: Vec<PathBuf>,
93}
94
95/// The error for a log with no active segment. [`Log::open`] makes one and
96/// every roll replaces it, so an open log always has one.
97fn no_active_segment() -> LogError { LogError::Corrupt("the log has no active segment".into()) }
98
99/// A Kafka-format log: a sorted collection of [`Segment`]s plus a single
100/// active segment that accepts appends.
101///
102/// `Log` has one writer and many concurrent readers. Mutation takes
103/// `&mut self`; `read`, `log_start_offset`, and the other read methods take
104/// `&self`. Construct one with [`Log::open`].
105#[derive(Debug)]
106pub struct Log {
107    dir: PathBuf,
108    config: Arc<RwLock<LogConfig>>,
109    io: Arc<dyn LogIo>,
110    segments: Vec<Segment>,
111    active: Option<Segment>,
112    dir_sync_needed: bool,
113    rollover_flusher: rollover_flush::Flusher,
114    /// The one compaction job this log allows at a time, and the fence's
115    /// hold on its build. See [`Log::prepare_compaction`].
116    compaction_gate: Arc<compaction::CompactionGate>,
117    /// Set by [`Log::fence`] when the partition is being removed: every
118    /// append from then on fails with [`LogError::Fenced`].
119    fenced: bool,
120    /// The log end offset when this process opened the log. The bytes from
121    /// here on in the active segment are ones this process wrote.
122    opened_end: Offset,
123    unflushed_messages: u64,
124    last_flush: Instant,
125    /// Bytes appended through the page cache since the last background sync
126    /// of the active segment started, and the syncs still running, oldest
127    /// first. See [`LogConfig::writeback_window`] and `Log::pace_writeback`.
128    /// `wasm32-wasip1` has no flush pool, so it paces nothing.
129    #[cfg(not(target_os = "wasi"))]
130    writeback_unsynced: u64,
131    #[cfg(not(target_os = "wasi"))]
132    writeback_jobs: VecDeque<rollover_flush::FlushTicket>,
133    /// Bytes of the active segment, from its start, that a background sync
134    /// has been handed so far; the next one covers from here. Only whole
135    /// pages are handed over. See `Log::pace_writeback`.
136    #[cfg(not(target_os = "wasi"))]
137    writeback_handed: u64,
138    /// How many times this log has cut a segment file in place
139    /// ([`Log::truncate_to`]). A read planned under the lock and run without
140    /// it ([`Log::plan_raw_read`]) takes the count and reads nothing if the
141    /// count has moved by the time it has its bytes, since a file cut and
142    /// written again under it can give it a header from before the cut with
143    /// bytes from after. Shared with the plans, so they can ask without the
144    /// lock.
145    truncations: Arc<AtomicU64>,
146    roll_jitter: Option<RollJitter>,
147    pending_deletes: Vec<PendingDelete>,
148    /// The global log start (Kafka's `logStartOffset`): the first offset any
149    /// reader may ask for, wherever the records for it live.
150    ///
151    /// It is a pointer of its own rather than a value derived from the
152    /// segments on disk, because KIP-405 gives a tiered partition two floors
153    /// and only one of them follows local files. `DeleteRecords`, ordinary
154    /// retention, a trim and a reset move this one; dropping a local segment
155    /// whose copy is in the remote tier does not, and that is what leaves the
156    /// band `[log_start_offset(), local_log_start_offset())` for the remote
157    /// read path to serve. [`Log::local_log_start_offset`] is the second
158    /// floor.
159    ///
160    /// The segment names witness only the local half. [`Log::open`] derives
161    /// this pointer from them and then restores the durable half from
162    /// `log-start-offset-checkpoint`, which [`Log::set_log_start_offset`]
163    /// writes at once and retention writes on a timer.
164    start_offset: Offset,
165
166    /// Whether [`Self::start_offset`] is a floor someone deleted up to, rather
167    /// than one derived from the segments that happened to be on disk at
168    /// [`Log::open`].
169    ///
170    /// True once this process moves the floor, and true at open when
171    /// `log-start-offset-checkpoint` restored one. False on a log whose only
172    /// witness is its segment names: on a tiered partition whose local
173    /// segments were evicted, that derivation sits *above* everything the
174    /// archive holds. Three callers must not act on such a floor: the remote
175    /// read would refuse offsets the archive can still serve, the log-start
176    /// breach in remote retention would delete the archive, and the
177    /// `ListOffsets(earliest)` clamp would report an offset past every record
178    /// the tier still holds. [`Log::established_log_start`] is what they ask
179    /// instead.
180    start_offset_established: bool,
181
182    /// The log start `log-start-offset-checkpoint` holds on disk, or `None`
183    /// when there is no such file.
184    checkpointed_start: Option<Offset>,
185
186    /// When the start that retention moved is due in
187    /// `log-start-offset-checkpoint`, or `None` when the file is current. See
188    /// [`Log::checkpoint_log_start_if_due`].
189    start_checkpoint_due: Option<Instant>,
190
191    /// How many of the leading sealed segments are a *clean* prefix, because
192    /// a compaction pass produced them and nothing has appended to the log's
193    /// dirty region since.
194    ///
195    /// Kafka keeps the same fact in `cleaner-offset-checkpoint`: the dirty
196    /// region is everything above the last cleaned offset, and on a log
197    /// nothing has cleaned yet that is the whole log. A pass groups every
198    /// cleanable sealed segment into one or more size-bounded output
199    /// segments (`segment.bytes`, Kafka's `Cleaner.groupSegmentsBySize`), and
200    /// every one of them was rewritten with the full offset map, so all of
201    /// them -- not just the first -- are the last pass's output and count as
202    /// clean. Tracking a count rather than a single boolean is what keeps
203    /// `min.cleanable.dirty.ratio` from treating every output segment past
204    /// the first as dirty and asking for a pass that would rewrite an
205    /// already-deduplicated segment for nothing.
206    ///
207    /// It is in memory only. A reopened log reads as never cleaned, which
208    /// costs one extra pass and never skips a needed one; Kafka's checkpoint
209    /// is durable, and making this one durable is the same change on both
210    /// sides of a restart.
211    clean_prefix_segments: usize,
212
213    /// First unstable offset: the first offset of the earliest transaction
214    /// that is open, or that is complete but whose marker is not yet below the
215    /// high watermark. It is `log_end_offset()` when there is no such
216    /// transaction. [`Log::last_stable_offset`] caps it at the high watermark.
217    lso: Offset,
218
219    /// In-flight transactions: `producer_id` → first offset of this
220    /// producer's currently-open txn. The log clears the entry when it
221    /// applies a commit or abort marker for that `producer_id`.
222    pending: pending_transactions::PendingTransactions,
223
224    /// KIP-890 verification state per producer, from the start of a
225    /// transaction verification to the producer's next transactional append.
226    /// See [`VerificationGuard`].
227    verification_states: HashMap<ProducerId, verification::VerificationState>,
228    /// Complete transactions whose marker the high watermark has not passed:
229    /// first offset of the transaction → last offset of its marker. They still
230    /// hold the last stable offset, as Kafka's `unreplicatedTxns` does, so a
231    /// `read_committed` reader never sees a transaction whose outcome a leader
232    /// change can still truncate away. [`Log::last_stable_offset`] releases
233    /// an entry once its marker is below the high watermark.
234    unreplicated: BTreeMap<Offset, Offset>,
235
236    /// Exact data-batch ranges for each in-flight transaction. A commit
237    /// marker stamps only these ranges, so interleaved transactions and
238    /// ordinary records never inherit another transaction's commit stamp.
239    pending_stamp_ranges: HashMap<ProducerId, Vec<OffsetRange>>,
240
241    /// Last transaction-coordinator epoch observed in a durable commit or
242    /// abort marker for each producer. The marker value carries this field,
243    /// so log recovery can rebuild it without a separate sidecar.
244    coordinator_epochs: HashMap<ProducerId, i32>,
245
246    /// Producer sequence, epoch, and transaction metadata persisted in
247    /// Kafka-compatible `.snapshot` files at segment boundaries.
248    producer_state: HashMap<ProducerId, ProducerSnapshotEntry>,
249
250    /// Up to four data batches each producer appended before the last one
251    /// its `producer_state` entry holds, at that entry's epoch, oldest first.
252    /// With the last batch they are Kafka's retained `BatchMetadata`. No
253    /// snapshot carries them, so a reopen rebuilds them from the replayed
254    /// tail alone.
255    earlier_batches: HashMap<ProducerId, VecDeque<ProducerBatchMetadata>>,
256
257    /// Active segment's `TxnIndex`. The log reopens it on segment roll.
258    active_txn_index: TxnIndex,
259
260    /// Immutable transaction indexes keyed by sealed segment base offset.
261    sealed_txn_indexes: BTreeMap<Offset, TxnIndex>,
262
263    /// Injected source of the additional internal stamp coordinate. `None`
264    /// is the default and means this partition stamps nothing. Behavior and
265    /// all wire-exact bytes stay exactly as they are without a stamp.
266    /// [`Log::set_stamp_source`] sets this field. The broker shares one tenant
267    /// source across all hosted partitions when internal stamping is enabled.
268    stamp_source: Option<Arc<dyn StampSource>>,
269
270    /// Open `.stampindex` sidecars keyed by segment base offset. Transactional
271    /// data can commit after its segment rolls, so the log keeps sealed
272    /// indexes available instead of retaining only the active one.
273    stamp_indexes: BTreeMap<Offset, StampIndex>,
274
275    /// Per-partition leader-epoch checkpoint. All segments share it, and
276    /// epoch history accumulates over the log's lifetime.
277    epoch_checkpoint: LeaderEpochCheckpoint,
278
279    /// External next-offset authority used by diskless recovery. The broker
280    /// sets this after it reads the committed `KRaft` frontier. Caller-supplied
281    /// append-at bases must then not be below
282    /// `max(log_end_offset, reconciled_frontier)`.
283    reconciled_frontier: Offset,
284
285    /// Cached deliver-at-time watermark: the first offset whose activation
286    /// time has not passed. It only ever moves forward, and it is derived
287    /// from the batch timestamps on disk, so nothing persists it.
288    /// [`Log::advance_delivery_watermark`] recomputes it.
289    delivery_watermark: Offset,
290
291    /// Activation time of the batch that stopped the last activation walk.
292    /// While that instant is still in the future, no walk can get past that
293    /// batch, so a repeat advance answers from this field and does no I/O.
294    /// `None` means the last walk found nothing waiting, or that a truncation
295    /// or a compaction may have removed the batch it named.
296    delivery_pending_ms: Option<i64>,
297}