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}