krabka_log/segment.rs
1//! A single segment: the `.log`, `.index`, and `.timeindex` files that share
2//! a base offset.
3
4use {
5 bytes::Bytes,
6 krabka_ids::Offset,
7 krabka_units::prelude::{ByteSize, ByteSizeExt},
8 std::{
9 fs::File,
10 path::{Path, PathBuf},
11 sync::Arc,
12 },
13};
14
15mod activation;
16mod append;
17mod direct;
18mod direct_mode;
19#[cfg(test)]
20mod dump_log_tests;
21mod header_walk;
22mod io;
23mod lifecycle;
24mod open;
25mod read;
26mod read_raw;
27#[krabka_macros::sendfile_cfg]
28mod read_raw_desc;
29mod read_source;
30mod sealed_input;
31#[cfg(test)]
32mod test_support;
33mod timestamp_scan;
34
35#[krabka_macros::sendfile_cfg]
36pub use self::read_raw_desc::RawDescSource;
37pub use self::{
38 append::PlacedBatch,
39 open::{FrontierScan, TailScan},
40 read_raw::RawReadSource,
41 read_source::{ReadSource, SegmentReadView},
42 sealed_input::SealedInput,
43};
44#[krabka_macros::sendfile_cfg]
45use krabka_protocol::records::FileRegion;
46use {
47 crate::{
48 index::{OFFSET_ENTRY_SIZE, OffsetIndex, TIME_ENTRY_SIZE, TimeIndex},
49 io::LogIo,
50 name::{leader_epoch_checkpoint_path, stampindex_path, txnindex_path},
51 },
52 std::{ops::Range, sync::atomic::AtomicBool, time::SystemTime},
53};
54
55/// A single log segment: the `.log` data file paired with its sparse
56/// `.index` (offset → byte position) and `.timeindex` (timestamp →
57/// relative offset) sidecars.
58///
59/// The `base_offset` identifies a segment. It is the absolute offset of the
60/// segment's first record, encoded into the segment's 20-digit zero-padded
61/// filename. [`Segment::create`] makes a new active segment.
62/// [`Segment::open`] validates and opens a sealed segment for reading, and
63/// [`Segment::open_active`] opens an active segment with tail recovery.
64/// The state of a segment's block reservation requests.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66enum ReserveRequest {
67 /// Nothing has been asked.
68 #[default]
69 None,
70 /// The segment size the last request worked from.
71 Size(u64),
72 /// The filesystem refused; the segment grows as it is written.
73 Refused,
74}
75
76#[derive(Debug)]
77pub struct Segment {
78 dir: PathBuf,
79 base_offset: Offset,
80 /// The `.log` data file, wrapped in `Arc`.
81 ///
82 /// The `Arc` lets the zero-copy fetch path (Increment D) hand a
83 /// `FileRegion { file: Arc<File>, .. }` to the connection's async
84 /// `sendfile` loop. The `Arc` pins the inode through the send even when
85 /// retention rolls or removes this segment in the meantime, because the
86 /// open fd keeps the inode alive on Unix. Writes and data syncs go through
87 /// `io`, which normally delegates straight to this file.
88 log_file: Arc<File>,
89 io: Arc<dyn LogIo>,
90 log_size: u64,
91 offset_index: OffsetIndex,
92 time_index: TimeIndex,
93 /// `true` once a new segment has started after this one. Sealed segments
94 /// do not accept appends.
95 sealed: bool,
96 /// Highest timestamp observed across all batches written here.
97 max_timestamp: i64,
98 /// Last absolute offset of the first batch that carried `max_timestamp`:
99 /// Kafka's `shallowOffsetOfMaxTimestampSoFar`. The time index pairs the
100 /// running maximum timestamp with this offset.
101 max_timestamp_offset: Offset,
102 /// First record's timestamp, cached for append-time segment rolling.
103 /// Truncation clears it so a replacement first batch gets a fresh clock.
104 first_timestamp: Option<i64>,
105 /// Last absolute offset (inclusive) of any batch in this segment.
106 last_offset: Offset,
107 /// End of the disk blocks the `.log` file may hold past `log_size`:
108 /// those a [`Segment::reserve`] reserved, or, for a segment opened from
109 /// disk, those the filesystem reports allocated. Sealing gives back the
110 /// range `log_size..reserved_end`. It is `0` when there is nothing to
111 /// give back.
112 reserved_end: u64,
113 /// What [`Segment::reserve`] has been asked and told, so a truncate can
114 /// take the reservation again for the same size and a filesystem that
115 /// refused is asked for no more.
116 reserve_requested: ReserveRequest,
117 /// The `O_DIRECT` writer and the newest bytes it wrote, while this
118 /// segment writes through `O_DIRECT`. See [`direct_mode`]. Boxed: every
119 /// sealed segment carries the field, and a log is held across `await`s.
120 direct: Option<Box<direct_mode::DirectMode>>,
121 /// Whether [`Segment::write_direct`] has asked for an `O_DIRECT` handle
122 /// since the segment last wrote through the page cache, granted or not.
123 direct_asked: bool,
124 /// Whether this segment has written through `O_DIRECT` since it was
125 /// opened, sealed or not. Its bytes are then mostly not in the page
126 /// cache, so a fetch reads them in one large read rather than walking
127 /// the batch headers one small read at a time and sending the run from
128 /// the file. See `read_raw_desc`.
129 wrote_direct: bool,
130 /// Unsupported page-cache probes are remembered per open segment: other
131 /// segments can live on filesystems with different NOWAIT capabilities.
132 /// Shared with the segment's read views, so a probe one of them finds
133 /// unsupported is not tried again by the next.
134 cache_probe_unsupported: Arc<AtomicBool>,
135}
136
137/// Verbatim, decode-free output of [`Segment::read_raw`].
138#[derive(Debug, Clone)]
139pub struct RawSegmentRead {
140 /// `base_offset` of the first included batch (≤ requested offset).
141 pub start_offset: Offset,
142 /// Last absolute offset covered by `bytes` (`start_offset - 1` if empty).
143 pub last_offset: Offset,
144 /// Verbatim `.log` bytes: one or more complete v2 batches.
145 pub bytes: Bytes,
146 /// The positions of `bytes` in the `.log` file when they were read from
147 /// it, so the log can drop the pages of a cold read; `None` when they
148 /// came from a direct-mode segment's memory, or the read is empty.
149 pub(crate) file_range: Option<Range<u64>>,
150}
151
152#[krabka_macros::sendfile_cfg]
153/// Descriptor form of [`Segment::read_raw`] for the zero-copy fetch path,
154/// Increments D + E.
155///
156/// This type carries the same offset and boundary metadata, but the
157/// records run is a [`krabka_protocol::records::FileRegion`] descriptor of
158/// the form `(Arc<File>, offset, len)`, not an owned `Bytes` slice. The
159/// broker can therefore `sendfile(2)` the run straight from the page cache
160/// with no userspace copy. This type is compiled on the SENDFILE alias:
161/// Linux, Apple, and FreeBSD/DragonFly.
162#[derive(Debug, Clone)]
163pub struct RawSegmentDesc {
164 /// `base_offset` of the first included batch (≤ requested offset).
165 pub start_offset: Offset,
166 /// Last absolute offset covered by the region (`start_offset - 1` if empty).
167 pub last_offset: Offset,
168 /// The records run, as a file-backed descriptor. `None` when the walk
169 /// found no complete batch in range.
170 pub region: Option<FileRegion>,
171}
172
173#[krabka_macros::sendfile_cfg]
174impl RawSegmentDesc {
175 fn empty() -> Self {
176 Self {
177 start_offset: Offset(0),
178 last_offset: Offset(-1),
179 region: None,
180 }
181 }
182
183 /// Byte length of the region. It is 0 when the region is empty.
184 #[must_use]
185 pub fn len(&self) -> usize { self.region.as_ref().map_or(0, |r| r.len) }
186
187 /// `true` when no batch bytes were described.
188 #[must_use]
189 pub fn is_empty(&self) -> bool { self.region.is_none() }
190}
191
192impl RawSegmentRead {
193 // cargo-mutants: the `last_offset` of an empty read is a never-read sentinel: every
194 // consumer guards on `is_empty()` (which checks `bytes`) before touching
195 // `last_offset`, so `Offset(-1)` vs `Offset(1)` is unobservable.
196 #[cfg_attr(test, mutants::skip)]
197 fn empty() -> Self {
198 Self {
199 start_offset: Offset(0),
200 last_offset: Offset(-1),
201 bytes: Bytes::new(),
202 file_range: None,
203 }
204 }
205
206 /// `true` when no batch bytes were returned.
207 #[must_use]
208 pub fn is_empty(&self) -> bool { self.bytes.is_empty() }
209}
210
211/// What an activation walk found in one segment.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct ActivationScan {
214 /// Exclusive end of the run of active batches that begins at the offset
215 /// the walk started from. It equals that offset when the first batch is
216 /// already waiting.
217 pub active_end: Offset,
218 /// Activation time of the first batch that is not active yet, or `None`
219 /// when every batch from the starting offset to the end of this segment
220 /// is active.
221 pub pending_at: Option<i64>,
222}
223
224impl Segment {
225 /// Absolute offset of the first record this segment can hold.
226 #[must_use]
227 pub fn base_offset(&self) -> Offset { self.base_offset }
228
229 /// Path to this segment's `.txnindex` file (may not exist yet).
230 #[must_use]
231 pub fn txn_index_path(&self) -> PathBuf { txnindex_path(&self.dir, self.base_offset.0) }
232
233 /// Path to this segment's `.stampindex` sidecar (may not exist yet).
234 #[must_use]
235 pub fn stamp_index_path(&self) -> PathBuf { stampindex_path(&self.dir, self.base_offset.0) }
236
237 /// Path to the per-partition `.leader-epoch-checkpoint` file in this
238 /// segment's directory. All segments in a partition share the checkpoint,
239 /// and epoch history accumulates over the log's lifetime.
240 #[must_use]
241 pub fn leader_epoch_checkpoint_path(&self) -> PathBuf {
242 leader_epoch_checkpoint_path(&self.dir)
243 }
244
245 /// Highest absolute offset (inclusive) of any batch appended to this
246 /// segment. Returns `base_offset - 1` for an empty segment.
247 #[must_use]
248 pub fn last_offset(&self) -> Offset { self.last_offset }
249
250 /// Current `.log` file size.
251 ///
252 /// The field itself stays a raw `u64`, because it is a file position and
253 /// goes directly into a `pread` or `seek` argument. It converts here,
254 /// where the value becomes a magnitude that the roll and retention
255 /// policies compare.
256 #[must_use]
257 pub fn size(&self) -> ByteSize { ByteSize::from_bytes(self.log_size) }
258
259 /// Whether the offset index or the time index is full under a
260 /// `segment.index.bytes` of `segment_index_size`, Kafka's
261 /// `offsetIndex().isFull() || timeIndex().isFull()`.
262 ///
263 /// Kafka sizes each index file to `segment.index.bytes` rounded down to
264 /// whole entries, so the offset index holds `size / 8` entries and the
265 /// time index `size / 12`. Kafka's `TimeIndex.isFull` keeps the last
266 /// slot free for the entry that sealing the segment writes, so the time
267 /// index is full one entry early.
268 #[must_use]
269 pub(crate) fn indexes_full(&self, segment_index_size: ByteSize) -> bool {
270 self.indexes_full_after(segment_index_size, 0)
271 }
272
273 /// Check capacity with at most one new entry in each index per batch.
274 pub(crate) fn indexes_full_after(&self, segment_index_size: ByteSize, batches: usize) -> bool {
275 let max_entries = |entry_size: usize| {
276 segment_index_size.bytes_u64() / u64::try_from(entry_size).unwrap_or(u64::MAX)
277 };
278 let entries =
279 |count: usize| u64::try_from(count.saturating_add(batches)).unwrap_or(u64::MAX);
280 entries(self.offset_index.entry_count()) >= max_entries(OFFSET_ENTRY_SIZE)
281 || entries(self.time_index.entry_count()).saturating_add(1)
282 >= max_entries(TIME_ENTRY_SIZE)
283 }
284
285 /// Highest timestamp observed across all batches in this segment.
286 /// Returns `i64::MIN` for an empty segment, and can also read `-1`
287 /// (`NO_TIMESTAMP`) when every batch in the segment carries no
288 /// timestamp. See [`Segment::last_modified`] for Kafka's fallback for
289 /// both cases.
290 #[must_use]
291 pub fn max_timestamp(&self) -> i64 { self.max_timestamp }
292
293 /// The `.log` file's last-modified time, as Kafka's `LogSegment.
294 /// lastModified` reads it. Time retention falls back to this when the
295 /// segment carries no record timestamp of its own (`max_timestamp()` is
296 /// `i64::MIN` or `-1`), the same as `UnifiedLog`'s
297 /// `largestRecordTimestamp().orElse(lastModified())`.
298 ///
299 /// `None` when the filesystem cannot report it, which retention treats
300 /// as "unknown, not overdue" rather than as license to guess.
301 #[must_use]
302 pub fn last_modified(&self) -> Option<SystemTime> {
303 self.log_file.metadata().ok()?.modified().ok()
304 }
305
306 /// `true` once [`Segment::seal`] has sealed the segment. Sealed segments
307 /// reject appends.
308 #[must_use]
309 pub fn is_sealed(&self) -> bool { self.sealed }
310
311 /// Directory that holds this segment's `.log`, `.index`, and `.timeindex`
312 /// files. The compactor uses it to read the `.log` file directly and to
313 /// avoid the `Segment::read` path. That path depends on the in-memory
314 /// `last_offset`, which is stale for sealed segments loaded through
315 /// the log's no-scan loader.
316 #[must_use]
317 pub fn dir(&self) -> &Path { &self.dir }
318}