Skip to main content

krabka_log/segment/
read_source.rs

1//! What a verbatim read needs of a segment, and a snapshot of it that a read
2//! can use once the log lock is released.
3//!
4//! A consumer that is catching up reads cold bytes: its fetch walks batch
5//! headers one small read at a time, asks the kernel to read a window ahead
6//! in 128 KiB hints, and copies or describes megabytes. Each of those is a
7//! system call, and some of them wait for the disk. Run under the log lock,
8//! as the log's own `read_raw` does, they hold up every append of the
9//! partition for as long as they take: milliseconds per fetch, which is what
10//! a producer sees as its publish p99 while a backlog drains.
11//!
12//! [`ReadSource`] is the view of a segment the verbatim read code runs over.
13//! The live [`Segment`] implements it on its fields, so a read under the lock
14//! works as before. [`SegmentReadView`] implements it on a snapshot taken
15//! under the lock: the file and I/O handles, the sizes and offsets, the
16//! direct-mode tail cache, and the sparse-index lookups the read's start
17//! position needs. The snapshot costs a few `Arc` clones and two binary
18//! searches, and the read then runs on it with the lock released.
19//!
20//! A view is of one moment. A sealed segment never changes under it. The
21//! active segment grows, and a view reads up to the size it saw: bytes
22//! appended after the plan belong to the next fetch.
23
24#[krabka_macros::sendfile_cfg]
25use std::sync::atomic::AtomicBool;
26use {
27    super::{Segment, direct::TailCache},
28    crate::{error::LogError, index::OffsetIndexEntry, io::LogIo},
29    bytes::Bytes,
30    krabka_ids::Offset,
31    std::{fs::File, sync::Arc},
32};
33
34/// The sparse-index lookups a read's start position needs, for one target
35/// offset of one segment.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct IndexHint {
38    /// The index entry at or before the target.
39    pub(super) floor: Option<OffsetIndexEntry>,
40    /// The `.log` position of the first index entry at or past the target,
41    /// which bounds the header walk that finds the target's batch.
42    pub(super) bound: Option<u64>,
43}
44
45/// The relative offset of `target` in a segment based at `base`, clamped at
46/// zero for a target before the segment. A target too far past the segment
47/// to fit the index's `u32` is an error only once a read needs it: a fetch
48/// that starts past the segment's last offset reads nothing from it.
49pub(super) fn relative_target(
50    base: Offset,
51    target: Offset,
52    operation: &str,
53) -> Result<u32, LogError> {
54    u32::try_from((target.0 - base.0).max(0))
55        .map_err(|_| LogError::Corrupt(format!("{operation} target offset out of range")))
56}
57
58/// What the verbatim read code reads a segment through.
59///
60/// Every method is a plain accessor, so the read code written against this
61/// trait is the same whether it runs over a live segment or a snapshot of
62/// one. The index lookups are the exception: a segment answers for any
63/// target, a view only for the target it was taken for.
64pub trait ReadSource {
65    /// The `.log` file.
66    fn file(&self) -> &Arc<File>;
67    /// The I/O the reads, hints and drops go through.
68    fn io(&self) -> &Arc<dyn LogIo>;
69    /// The bytes of the `.log` file a read may use.
70    fn log_size(&self) -> u64;
71    /// The segment's base offset.
72    fn base(&self) -> Offset;
73    /// The last offset of any batch in the segment.
74    fn last(&self) -> Offset;
75    /// Whether the segment has written through `O_DIRECT`, so its bytes are
76    /// mostly not in the page cache. Only the descriptor read asks, so it
77    /// exists where `sendfile` does.
78    #[krabka_macros::sendfile_cfg]
79    fn wrote_direct(&self) -> bool;
80    /// The newest bytes of a segment that writes directly, held in memory.
81    fn tail_cache(&self) -> Option<&TailCache>;
82    /// Whether the page-cache probe is known not to work on this segment's
83    /// filesystem; see the descriptor read's `byte_is_cached`, its only user.
84    #[krabka_macros::sendfile_cfg]
85    fn cache_probe_unsupported(&self) -> &AtomicBool;
86    /// The index lookups for a read whose target is `target_rel`.
87    fn index_hint(&self, target_rel: u32) -> IndexHint;
88
89    /// The bytes from `pos` the segment holds in memory, when it writes
90    /// directly and the tail cache reaches `pos`.
91    fn cached_bytes(&self, pos: u64, max: usize) -> Option<Bytes> {
92        self.tail_cache()?.bytes_from(pos, max)
93    }
94}
95
96/// The accessors a segment and its view answer from fields of the same
97/// name: the view copies them from the segment.
98macro_rules! field_accessors {
99    () => {
100        fn file(&self) -> &Arc<File> { &self.log_file }
101
102        fn io(&self) -> &Arc<dyn LogIo> { &self.io }
103
104        fn log_size(&self) -> u64 { self.log_size }
105
106        fn base(&self) -> Offset { self.base_offset }
107
108        fn last(&self) -> Offset { self.last_offset }
109
110        #[krabka_macros::sendfile_cfg]
111        fn wrote_direct(&self) -> bool { self.wrote_direct }
112    };
113}
114
115impl ReadSource for Segment {
116    field_accessors!();
117
118    fn tail_cache(&self) -> Option<&TailCache> { self.direct.as_ref().map(|mode| &mode.cache) }
119
120    #[krabka_macros::sendfile_cfg]
121    fn cache_probe_unsupported(&self) -> &AtomicBool { &self.cache_probe_unsupported }
122
123    fn index_hint(&self, target_rel: u32) -> IndexHint {
124        IndexHint {
125            floor: self.offset_index.floor_entry(target_rel),
126            bound: self
127                .offset_index
128                .position_at_or_after(target_rel)
129                .map(u64::from),
130        }
131    }
132}
133
134/// A segment as a read from one offset sees it, taken under the log lock so
135/// the read can run without it. See the module documentation.
136#[derive(Debug, Clone)]
137pub struct SegmentReadView {
138    log_file: Arc<File>,
139    io: Arc<dyn LogIo>,
140    log_size: u64,
141    base_offset: Offset,
142    last_offset: Offset,
143    #[cfg(any(
144        target_os = "linux",
145        target_os = "macos",
146        target_os = "ios",
147        target_os = "tvos",
148        target_os = "watchos",
149        target_os = "freebsd",
150        target_os = "dragonfly",
151    ))]
152    wrote_direct: bool,
153    tail: Option<TailCache>,
154    #[cfg(any(
155        target_os = "linux",
156        target_os = "macos",
157        target_os = "ios",
158        target_os = "tvos",
159        target_os = "watchos",
160        target_os = "freebsd",
161        target_os = "dragonfly",
162    ))]
163    cache_probe_unsupported: Arc<AtomicBool>,
164    /// The target the index hint is for: the read's offset, relative.
165    target_rel: u32,
166    hint: IndexHint,
167    /// The `.log` position before which a fetch of this segment is cold, so
168    /// the pages it reads in are dropped behind it; `None` when no fetch of
169    /// it is. See `Log::cold_before`.
170    cold_before: Option<u64>,
171}
172
173impl SegmentReadView {
174    /// The `.log` position before which a fetch of the segment is cold, or
175    /// `None` when no fetch of it is.
176    pub fn cold_before(&self) -> Option<u64> { self.cold_before }
177
178    /// Whether the read starts in bytes the page-cache budget no longer
179    /// holds: its index floor, at or before where it starts, lies before the
180    /// position the segment turns cold at. Such a read waits for the disk,
181    /// whatever the segment's own verdict on its recent bytes.
182    pub fn starts_cold(&self) -> bool {
183        self.cold_before
184            .is_some_and(|cold| self.floor_position() < cold)
185    }
186
187    /// The `.log` position of the index floor of the view's target, or 0.
188    fn floor_position(&self) -> u64 { self.hint.floor.map_or(0, |entry| u64::from(entry.position)) }
189
190    /// The bytes from the index floor of the view's target to the end of
191    /// the segment: at least what a read from the target can take from it,
192    /// since the read starts at or after the floor.
193    pub fn bytes_from_floor(&self) -> u64 { self.log_size.saturating_sub(self.floor_position()) }
194
195    /// Drop the `len` bytes of the `.log` file from `offset` from the page
196    /// cache, in whole pages: [`LogIo::drop_cached`]. A fetch of bytes the
197    /// page-cache budget no longer holds calls this on the pages behind it,
198    /// so a consumer reading an old backlog leaves none in the cgroup.
199    #[cfg(not(target_os = "wasi"))]
200    pub fn drop_cached(&self, offset: u64, len: u64) {
201        self.io.drop_cached(&self.log_file, offset, len);
202    }
203}
204
205impl ReadSource for SegmentReadView {
206    field_accessors!();
207
208    fn tail_cache(&self) -> Option<&TailCache> { self.tail.as_ref() }
209
210    #[krabka_macros::sendfile_cfg]
211    fn cache_probe_unsupported(&self) -> &AtomicBool { &self.cache_probe_unsupported }
212
213    fn index_hint(&self, target_rel: u32) -> IndexHint {
214        assert2::debug_assert!(
215            target_rel == self.target_rel,
216            "a read view answers for the offset it was taken for"
217        );
218        self.hint
219    }
220}
221
222impl Segment {
223    /// A view of this segment for a read from `fetch_offset`, to run after
224    /// the log lock is released. `cold_before` is the log's verdict on where
225    /// a fetch of this segment turns cold, taken now because it needs the
226    /// log.
227    ///
228    /// # Errors
229    /// Returns an error when `fetch_offset` is too far past the segment for
230    /// its index, which no read of the segment could use either.
231    pub fn read_view(
232        &self,
233        fetch_offset: Offset,
234        cold_before: Option<u64>,
235    ) -> Result<SegmentReadView, LogError> {
236        let target_rel = relative_target(self.base_offset, fetch_offset, "read_view")?;
237        Ok(SegmentReadView {
238            log_file: Arc::clone(&self.log_file),
239            io: Arc::clone(&self.io),
240            log_size: self.log_size,
241            base_offset: self.base_offset,
242            last_offset: self.last_offset,
243            #[cfg(any(
244                target_os = "linux",
245                target_os = "macos",
246                target_os = "ios",
247                target_os = "tvos",
248                target_os = "watchos",
249                target_os = "freebsd",
250                target_os = "dragonfly",
251            ))]
252            wrote_direct: self.wrote_direct,
253            tail: self.direct.as_ref().map(|mode| mode.cache.clone()),
254            #[cfg(any(
255                target_os = "linux",
256                target_os = "macos",
257                target_os = "ios",
258                target_os = "tvos",
259                target_os = "watchos",
260                target_os = "freebsd",
261                target_os = "dragonfly",
262            ))]
263            cache_probe_unsupported: Arc::clone(&self.cache_probe_unsupported),
264            target_rel,
265            hint: self.index_hint(target_rel),
266            cold_before,
267        })
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    #[krabka_macros::sendfile_cfg]
274    use crate::segment::RawDescSource as _;
275    use {
276        super::*,
277        crate::segment::{
278            RawReadSource as _,
279            test_support::{DENSE_INDEX, TempSegment, test_batch_at, test_segment},
280        },
281        krabka_units::prelude::{bytes, mebibytes},
282    };
283
284    /// A view taken under the lock reads what the segment reads, through
285    /// both verbatim paths, from every offset, and keeps reading it after
286    /// the segment has grown: it is of the moment it was taken.
287    #[krabka_macros::sendfile_cfg]
288    #[test]
289    fn a_view_reads_what_its_segment_read_when_it_was_taken() {
290        let TempSegment {
291            dir: _dir,
292            segment: mut seg,
293        } = test_segment();
294        for off in 0..12i64 {
295            seg.append(&test_batch_at(Offset(off)), DENSE_INDEX)
296                .unwrap();
297        }
298        let limit = Offset(12);
299        for fetch in 0..=12i64 {
300            for budget in [bytes(1), bytes(200), mebibytes(1)] {
301                let fetch = Offset(fetch);
302                let view = seg.read_view(fetch, None).unwrap();
303                let from_segment = seg.read_raw(fetch, limit, budget).unwrap();
304                let from_view = view
305                    .read_raw_with_policy(fetch, limit, budget, mebibytes(4), mebibytes(4))
306                    .unwrap();
307                assert2::check!(
308                    from_view.bytes == from_segment.bytes,
309                    "{fetch:?} {budget:?}"
310                );
311                assert2::check!(from_view.start_offset == from_segment.start_offset);
312                assert2::check!(from_view.last_offset == from_segment.last_offset);
313                let described = view
314                    .read_raw_desc_if_cached(fetch, limit, budget, mebibytes(4))
315                    .unwrap()
316                    .expect("a freshly written segment is cached");
317                assert2::check!(
318                    described.region.as_ref().map(|region| region.len)
319                        == Some(from_segment.bytes.len()).filter(|len| *len > 0)
320                );
321            }
322        }
323
324        // Appends after the view do not reach it: it reads to the size it saw.
325        let view = seg.read_view(Offset(0), None).unwrap();
326        let before = seg.read_raw(Offset(0), Offset(99), mebibytes(1)).unwrap();
327        seg.append(&test_batch_at(Offset(12)), DENSE_INDEX).unwrap();
328        let after = view
329            .read_raw_with_policy(
330                Offset(0),
331                Offset(99),
332                mebibytes(1),
333                mebibytes(4),
334                mebibytes(4),
335            )
336            .unwrap();
337        assert2::check!(after.bytes == before.bytes);
338        assert2::check!(after.last_offset == Offset(11));
339    }
340
341    /// The relative target clamps at the segment start and refuses a target
342    /// the index cannot hold.
343    #[test]
344    fn the_relative_target_clamps_below_and_refuses_overflow() {
345        assert2::check!(relative_target(Offset(10), Offset(3), "test").unwrap() == 0);
346        assert2::check!(relative_target(Offset(10), Offset(17), "test").unwrap() == 7);
347        assert2::check!(
348            relative_target(Offset(0), Offset(i64::from(u32::MAX) + 1), "test").is_err()
349        );
350    }
351}