Skip to main content

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}