krabka_log/log/read.rs
1//! The read paths: decoded batches, verbatim wire bytes, and file-region
2//! descriptors for the zero-copy `sendfile` fetch.
3//!
4//! All three find the first segment that can hold the fetch offset by a
5//! binary search over the sealed segments, then walk on from it through the
6//! later sealed segments and the active one, so a read can span a segment
7//! boundary. All three ask each segment for at least one whole batch header
8//! to satisfy Kafka's anti-stall rule. A read goes on into the next segment
9//! only after it reads the segment before it to the end. Only the first batch
10//! of a read can be larger than its budget.
11
12#[cfg(not(target_os = "wasi"))]
13use super::page_cache;
14use {
15 super::Log,
16 crate::{
17 error::LogError,
18 segment::{RawReadSource as _, ReadSource, Segment, SegmentReadView},
19 },
20 bytes::{Bytes, BytesMut},
21 krabka_ids::Offset,
22 krabka_protocol::records::{HEADER_LEN, RecordBatch},
23 krabka_units::prelude::{ByteSize, ByteSizeExt, bytes},
24 std::{
25 ops::Range,
26 sync::{
27 Arc, PoisonError,
28 atomic::{AtomicU64, Ordering},
29 },
30 },
31 tracing::{field, instrument},
32};
33#[krabka_macros::sendfile_cfg]
34use {crate::segment::RawDescSource as _, krabka_protocol::records::FileRegion, std::cell::Cell};
35
36/// A `usize` byte length as a quantity.
37///
38/// The read paths track their budget as a [`ByteSize`], but the lengths they
39/// subtract come from `Bytes::len` and `FileRegion::len`, which are `usize`.
40fn size_from_len(len: usize) -> ByteSize {
41 ByteSize::from_bytes(u64::try_from(len).unwrap_or(u64::MAX))
42}
43
44/// The fixed v2 record-batch header, as a quantity.
45///
46/// The log asks every per-segment read for at least this much, so the header
47/// walk that finds batch boundaries always has a header to read. This is
48/// Kafka's anti-stall rule. The value comes from [`HEADER_LEN`] rather than a
49/// restated constant, so the floor cannot drift from the protocol.
50fn batch_header() -> ByteSize { size_from_len(HEADER_LEN) }
51
52/// Whether a read that took a run out of `segment` can go on into the next
53/// segment.
54///
55/// `next` is the offset after the run. The read can go on only when the run
56/// reached the end of the segment. A run that stopped before the end stopped
57/// on the budget or on the limit. The next segment starts after batches that
58/// this segment still holds. If the read went on, the response would put the
59/// first batch of the next segment after the run, and skip every offset
60/// between them. Kafka's `LocalLog.read` never leaves the segment that it
61/// found data in.
62fn emptied(segment: &impl ReadSource, next: Offset) -> bool { next > segment.last() }
63
64/// How far behind a cold fetch of up to `max_size` bytes the pages its
65/// reader's previous fetch brought in can start: twice the size, since only
66/// a fetch's first batch may be larger than the size it asked for. See
67/// `Log::drop_cold`.
68fn cold_lookback(max_size: ByteSize) -> u64 { max_size.bytes_u64().saturating_mul(2) }
69
70/// Whether a run of `len` bytes fits the `remaining` budget of a read that
71/// already has its first batch.
72///
73/// Each segment read gives its first batch whole, however small its budget.
74/// That is Kafka's anti-stall rule, and KIP-74 applies it only to the first
75/// batch of a read. A later run that is larger than the remaining budget
76/// waits for the next read.
77fn fits_after_first(len: ByteSize, remaining: ByteSize) -> bool { len <= remaining }
78
79/// The progress of a raw read and the chunks it collected.
80struct RawChunks<T> {
81 cursor: RawReadCursor,
82 chunks: Vec<T>,
83}
84
85/// Shared progress for verbatim bytes and file regions, including the first-batch exception.
86struct RawReadCursor {
87 start: Offset,
88 next: Offset,
89 remaining: ByteSize,
90 last: Option<Offset>,
91}
92
93impl RawReadCursor {
94 fn new(offset: Offset, budget: ByteSize) -> Self {
95 Self {
96 start: offset,
97 next: offset,
98 remaining: budget,
99 last: None,
100 }
101 }
102
103 fn budget(&self) -> ByteSize { self.remaining.max(batch_header()) }
104
105 fn accept(&mut self, start: Offset, last: Offset, bytes: usize) -> bool {
106 let len = size_from_len(bytes);
107 if self.last.is_some() && !fits_after_first(len, self.remaining) {
108 return false;
109 }
110 if self.last.is_none() {
111 self.start = start;
112 }
113 self.remaining = (self.remaining - len).max(ByteSize::ZERO);
114 self.next = last + 1;
115 self.last = Some(last);
116 true
117 }
118
119 fn finished(&self, segment: &impl ReadSource, limit: Offset) -> bool {
120 self.remaining == ByteSize::ZERO || self.next >= limit || !emptied(segment, self.next)
121 }
122}
123
124/// A complete segment result with its offset span and byte cost.
125struct RawChunk<T> {
126 data: T,
127 start: Offset,
128 last: Offset,
129 len: usize,
130}
131
132/// Result of [`Log::read`]: the absolute offset of the first batch
133/// returned and the batches themselves.
134///
135/// `start_offset` falls back to the requested offset when the read returns
136/// no batches, for example a read at the log end. Callers can then resume
137/// from the value and need no special case for an empty result.
138#[derive(Debug)]
139pub struct ReadOutput {
140 /// Absolute offset of the first record in [`Self::batches`], or the
141 /// requested offset when no batches were returned.
142 pub start_offset: Offset,
143 /// Decoded batches in offset order. May be empty if the log has no
144 /// data at or after the requested offset.
145 pub batches: Vec<RecordBatch>,
146}
147
148/// Verbatim, decode-free output of [`Log::read_raw`].
149#[derive(Debug, Clone)]
150pub struct RawRead {
151 /// Absolute offset of the first batch in [`Self::bytes`], or the
152 /// requested offset when no bytes were returned.
153 pub start_offset: Offset,
154 /// Verbatim `.log` bytes: zero or more complete v2 batches. The bytes
155 /// can span segment boundaries.
156 pub bytes: Bytes,
157 /// Length of [`Self::bytes`] in bytes.
158 pub total: usize,
159 /// Last offset included in [`Self::bytes`], or `None` when empty.
160 pub last_offset: Option<Offset>,
161}
162
163#[krabka_macros::sendfile_cfg]
164/// Descriptor form of [`Log::read_raw`] for the zero-copy `sendfile` fetch
165/// path, Increments D + E.
166///
167/// One [`krabka_protocol::records::FileRegion`] describes the records run
168/// for each contributing segment. A multi-segment fetch therefore goes
169/// out as several `sendfile` regions with **no** coalescing copy.
170/// `read_raw` instead concatenates cross-segment chunks into a fresh
171/// `BytesMut`. This type is compiled on the SENDFILE alias: Linux, Apple,
172/// and FreeBSD/DragonFly.
173#[derive(Debug, Clone)]
174pub struct RawReadDesc {
175 /// Absolute offset of the first batch in the regions, or the requested
176 /// offset when no bytes were returned.
177 pub start_offset: Offset,
178 /// One file-backed region per contributing segment, in wire order.
179 pub regions: Vec<FileRegion>,
180 /// Total byte length across all regions.
181 pub total: usize,
182}
183
184impl Log {
185 /// The index in `self.segments` of the first sealed segment that can hold
186 /// `offset`, or `self.segments.len()` when none can and a read starts in
187 /// the active segment.
188 ///
189 /// That is the first sealed segment whose last offset is at or past
190 /// `offset`: the segment a walk from the oldest one would stop skipping
191 /// at. The search is a binary search, so a tail fetch does not cost a
192 /// step for every retained segment.
193 ///
194 /// It is exact because sealed last offsets never decrease. Bases strictly
195 /// increase, and a segment's last offset lies between its own base less
196 /// one, for an empty segment, and the next base less one. A compacted gap
197 /// lowers a last offset only within that range, and a reopened segment
198 /// takes the next base less one.
199 fn first_sealed_from(&self, offset: Offset) -> usize {
200 self.segments
201 .partition_point(|segment| segment.last_offset() < offset)
202 }
203
204 /// The segments a read from `offset` walks, in offset order: the sealed
205 /// ones from [`Self::first_sealed_from`] on, then the active one.
206 fn segments_from(&self, offset: Offset) -> impl Iterator<Item = &Segment> {
207 self.segments
208 .iter()
209 .skip(self.first_sealed_from(offset))
210 .chain(self.active.as_ref())
211 }
212
213 /// Plan a raw read from `fetch_offset`, to run once the lock on this log
214 /// is released.
215 ///
216 /// A fetch of cold bytes walks batch headers, asks the kernel to read
217 /// ahead and copies or describes megabytes, and all of it waits on the
218 /// disk or costs system calls: milliseconds per fetch of a consumer that
219 /// is catching up. Under the log lock those milliseconds hold up every
220 /// append of the partition, which a producer sees as its publish p99
221 /// while a backlog drains. The plan takes what the read needs of the
222 /// log in a few `Arc` clones and index lookups, and the read then runs
223 /// on it without the log: [`RawReadPlan::read_bytes`] and
224 /// [`RawReadPlan::read_regions`] give what [`Log::read_raw`] and
225 /// [`Log::read_raw_desc`] would, as of the moment of the plan.
226 ///
227 /// The plan views each segment the read may walk, from the one that
228 /// holds `fetch_offset` on, as [`RawReadPlan`] walks them: a later
229 /// segment is read from the offset after the one before it, once that one
230 /// is read to its end, so every view's offset is known now. Planning stops
231 /// once the views can fill `max_size`, counting each from its index floor,
232 /// which is at or before where its read starts.
233 ///
234 /// # Errors
235 /// Returns [`LogError::OffsetTooLow`] below the log start and
236 /// [`LogError::OffsetBelowLocalStart`] below the local one, as the reads
237 /// do, and an error for an offset too far past a segment for its index.
238 pub fn plan_raw_read(
239 &self,
240 fetch_offset: Offset,
241 limit_offset: Offset,
242 max_size: ByteSize,
243 ) -> Result<RawReadPlan, LogError> {
244 let (read_buffer_cap, read_ahead_max, budget) = {
245 let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
246 (
247 config.read_buffer_cap,
248 config.read_ahead_max,
249 config.page_cache_budget,
250 )
251 };
252 self.check_locally_readable(fetch_offset)?;
253 let mut plan = RawReadPlan {
254 fetch_offset,
255 limit_offset,
256 max_size,
257 read_buffer_cap,
258 read_ahead_max,
259 views: Vec::new(),
260 may_block: self.read_may_block(fetch_offset),
261 truncations: Arc::clone(&self.truncations),
262 truncations_seen: self.truncations.load(Ordering::SeqCst),
263 };
264 if fetch_offset >= limit_offset {
265 return Ok(plan);
266 }
267 let mut next = fetch_offset;
268 let mut planned = 0_u64;
269 for segment in self.segments_from(fetch_offset) {
270 if segment.last_offset() < next {
271 continue;
272 }
273 #[cfg(not(target_os = "wasi"))]
274 let cold_before = self.cold_before(segment, budget);
275 #[cfg(target_os = "wasi")]
276 let cold_before = {
277 let _ = budget;
278 None
279 };
280 let view = segment.read_view(next, cold_before)?;
281 planned = planned.saturating_add(view.bytes_from_floor());
282 plan.views.push(view);
283 if planned >= max_size.bytes_u64() {
284 break;
285 }
286 next = segment.last_offset() + 1;
287 if next >= limit_offset {
288 break;
289 }
290 }
291 // The segment's own verdict knows the page cache and its direct-write
292 // tail, not the budget: pages of the active segment the budget has
293 // dropped are on the disk, and a read that starts in them waits.
294 plan.may_block |= plan.views.first().is_some_and(SegmentReadView::starts_cold);
295 Ok(plan)
296 }
297
298 #[krabka_macros::sendfile_cfg]
299 /// Descriptor variant of [`Log::read_raw`] for the zero-copy `sendfile`
300 /// fetch path.
301 ///
302 /// This method walks sealed segments and then the active segment exactly
303 /// as `read_raw` does. It collects one [`krabka_protocol::records::FileRegion`] for each contributing segment
304 /// through `Segment::read_raw_desc`, instead of owned `Bytes`.
305 /// Multi-segment fetches are **not** coalesced. Each region goes out in
306 /// its own `sendfile` call, so the cross-segment copy disappears.
307 ///
308 /// The selected byte ranges are byte-identical to what `read_raw` would
309 /// have returned for the same `(fetch_offset, limit_offset, max_size)`.
310 /// It plans and runs under the caller's lock: see [`Log::plan_raw_read`]
311 /// for the fetch path's split.
312 #[instrument(
313 level = "debug",
314 skip(self),
315 fields(regions = field::Empty, total = field::Empty),
316 err,
317)]
318 /// # Errors
319 /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
320 pub fn read_raw_desc(
321 &self,
322 fetch_offset: Offset,
323 limit_offset: Offset,
324 max_size: ByteSize,
325 ) -> Result<RawReadDesc, LogError> {
326 self.plan_raw_read(fetch_offset, limit_offset, max_size)?
327 .read_regions()
328 }
329
330 /// The position in `segment`'s `.log` file before which a fetch is cold:
331 /// the page-cache budget no longer holds those bytes, so the pages a fetch
332 /// of them reads in would stay in the cgroup until its limit evicted
333 /// them. `None` when no fetch of the segment is cold: there is no budget.
334 ///
335 /// The budget's ledger drops a file's oldest pages first, so the pages of
336 /// a file it holds are the newest of those handed over, and everything
337 /// before the oldest of them is cold. A sealed segment with no page in the
338 /// ledger is cold throughout: its pages were handed over and dropped, or
339 /// it was written before this process opened the log. (The partial
340 /// write-back window at its end was never handed over and may still be
341 /// cached; a fetch of it counts as cold all the same, and a second reader
342 /// of those few pages reads the disk.) The active segment is cold up to
343 /// where its background syncs have handed pages over, once the ledger
344 /// holds none of them, and hot from there: those pages are its newest.
345 #[cfg(not(target_os = "wasi"))]
346 fn cold_before(&self, segment: &Segment, budget: Option<ByteSize>) -> Option<u64> {
347 budget?;
348 let retained_from = self
349 .rollover_flusher
350 .page_cache_retained_from(segment.log_file());
351 Some(match retained_from {
352 Some(from) => from,
353 None if segment.is_sealed() => u64::MAX,
354 None => self.writeback_handed,
355 })
356 }
357
358 /// Reject an offset no local segment can answer.
359 ///
360 /// Below the global log start there is nothing anywhere, and the error
361 /// says so. Between the global floor and the local one (KIP-405) the
362 /// records are in the remote tier: the read fails all the same, so the
363 /// broker's fetch path falls through to the remote reader rather than
364 /// serving the first batch a surviving segment happens to start with.
365 fn check_locally_readable(&self, offset: Offset) -> Result<(), LogError> {
366 let log_start = self.log_start_offset();
367 if offset < log_start {
368 return Err(LogError::OffsetTooLow {
369 requested: offset,
370 log_start,
371 });
372 }
373 let local_log_start = self.local_log_start_offset();
374 if offset < local_log_start {
375 return Err(LogError::OffsetBelowLocalStart {
376 requested: offset,
377 local_log_start,
378 });
379 }
380 Ok(())
381 }
382
383 /// Read batches from `offset` and return up to `max_size` of `.log`
384 /// data.
385 ///
386 /// The read walks sealed segments first, then the active segment, so a
387 /// read can span segment boundaries. It goes on into a segment only after
388 /// it reads the segment before it to the end.
389 #[instrument(
390 level = "debug",
391 skip(self),
392 fields(batches = field::Empty),
393 err,
394 )]
395 /// # Errors
396 /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
397 pub fn read(&self, offset: Offset, max_size: ByteSize) -> Result<ReadOutput, LogError> {
398 let log_end = self.log_end_offset();
399 self.check_locally_readable(offset)?;
400 if offset >= log_end {
401 return Ok(ReadOutput {
402 start_offset: log_end,
403 batches: Vec::new(),
404 });
405 }
406
407 let read_buffer_cap = self
408 .config
409 .read()
410 .unwrap_or_else(PoisonError::into_inner)
411 .read_buffer_cap;
412 let mut batches: Vec<RecordBatch> = Vec::new();
413 let mut current_offset = offset;
414 let mut remaining = max_size;
415
416 for seg in self.segments_from(offset) {
417 if seg.last_offset() < current_offset {
418 continue;
419 }
420 let bs =
421 seg.read_with_buffer_cap(current_offset, remaining.max(bytes(1)), read_buffer_cap)?;
422 let Some(last) = bs.last() else {
423 continue;
424 };
425 current_offset = Offset(last.base_offset + i64::from(last.last_offset_delta) + 1);
426 let consumed: usize = bs.iter().map(RecordBatch::encoded_len).sum();
427 remaining = (remaining - size_from_len(consumed)).max(ByteSize::ZERO);
428 batches.extend(bs);
429 if remaining == ByteSize::ZERO || !emptied(seg, current_offset) {
430 break;
431 }
432 }
433
434 let start_offset = batches.first().map_or(offset, |b| Offset(b.base_offset));
435 tracing::Span::current().record("batches", batches.len());
436 Ok(ReadOutput {
437 start_offset,
438 batches,
439 })
440 }
441
442 /// Like [`Log::read`], but returns verbatim wire bytes with no decode.
443 ///
444 /// The read walks sealed segments and then the active segment. It goes on
445 /// into a segment only after it reads the segment before it to the end.
446 /// It includes only batches with `base_offset < limit_offset`, up to
447 /// `max_size`, and always at least one batch. The first batch is included
448 /// whole, even when it is larger than `max_size`. It plans and runs under
449 /// the caller's lock: see [`Log::plan_raw_read`] for the fetch path's
450 /// split.
451 #[instrument(
452 level = "debug",
453 skip(self),
454 fields(total = field::Empty),
455 err,
456 )]
457 /// # Errors
458 /// Returns an error when log I/O fails, a record or index is corrupt, or the requested offset violates the segment state.
459 pub fn read_raw(
460 &self,
461 fetch_offset: Offset,
462 limit_offset: Offset,
463 max_size: ByteSize,
464 ) -> Result<RawRead, LogError> {
465 self.plan_raw_read(fetch_offset, limit_offset, max_size)?
466 .read_bytes()
467 }
468}
469
470/// A raw read planned under the log lock, to run without it.
471///
472/// The plan holds a view of each segment the read may walk, taken by
473/// [`Log::plan_raw_read`], and the read's bounds and policy. Running it is
474/// the header walks, the readahead hints, the copies or descriptions and the
475/// drops of cold pages that [`Log::read_raw`] and [`Log::read_raw_desc`] do,
476/// and it needs nothing of the log: the views hold the file handles, and
477/// read up to the sizes they saw.
478#[derive(Debug, Clone)]
479pub struct RawReadPlan {
480 fetch_offset: Offset,
481 limit_offset: Offset,
482 max_size: ByteSize,
483 read_buffer_cap: ByteSize,
484 read_ahead_max: ByteSize,
485 views: Vec<SegmentReadView>,
486 may_block: bool,
487 /// The log's truncation count, and its value when the plan was taken:
488 /// see `Log::truncations`.
489 truncations: Arc<AtomicU64>,
490 truncations_seen: u64,
491}
492
493impl RawReadPlan {
494 /// Whether running the plan may wait on more than the page cache:
495 /// [`Log::read_may_block`] for the read's offset, as of the plan. A
496 /// caller that must not block a runtime thread runs such a plan away
497 /// from it.
498 #[must_use]
499 pub fn may_block(&self) -> bool { self.may_block }
500
501 /// The offset the read cannot reach: the end of the last segment the plan
502 /// may walk, or the read's limit, whichever is lower; the read's own
503 /// offset when there is nothing to walk. A caller that needs the log's
504 /// view of the bytes the read may serve, such as the aborted transactions
505 /// a `read_committed` fetch reports beside them, takes it for this range
506 /// under the lock with the plan: bounded by the segment or two the read
507 /// may touch, not by the whole log behind it.
508 #[must_use]
509 pub fn offset_bound(&self) -> Offset {
510 self.views.last().map_or(self.fetch_offset, |view| {
511 (view.last() + 1).min(self.limit_offset)
512 })
513 }
514
515 /// Whether the log has cut a segment file since the plan was taken. The
516 /// bytes a read copied by then may mix what stood before the cut with
517 /// what was written after it, so the read serves nothing, and the next
518 /// fetch plans again against the log as it is now.
519 fn truncated_since(&self) -> bool {
520 self.truncations.load(Ordering::SeqCst) != self.truncations_seen
521 }
522
523 /// Run the plan as [`Log::read_raw`] would have read.
524 ///
525 /// # Errors
526 /// Returns an error when log I/O fails or a record or index is corrupt.
527 pub fn read_bytes(&self) -> Result<RawRead, LogError> {
528 let lookback = cold_lookback(self.max_size);
529 let RawChunks { cursor, mut chunks } = self.chunks(|view, next, remaining| {
530 let read = view.read_raw_with_policy(
531 next,
532 self.limit_offset,
533 remaining,
534 self.read_buffer_cap,
535 self.read_ahead_max,
536 )?;
537 if read.is_empty() {
538 return Ok(None);
539 }
540 if let Some(file_range) = &read.file_range {
541 drop_cold(view, file_range.clone(), lookback, true);
542 }
543 Ok(Some(RawChunk {
544 len: read.bytes.len(),
545 data: read.bytes,
546 start: read.start_offset,
547 last: read.last_offset,
548 }))
549 })?;
550
551 if self.truncated_since() {
552 chunks.clear();
553 }
554 let bytes = match chunks.len() {
555 // No chunk is empty bytes, and one chunk is itself.
556 0 | 1 => chunks.pop().unwrap_or_default(),
557 _ => {
558 let total: usize = chunks.iter().map(Bytes::len).sum();
559 let mut b = BytesMut::with_capacity(total);
560 for c in &chunks {
561 b.extend_from_slice(c);
562 }
563 b.freeze()
564 }
565 };
566 let total = bytes.len();
567 tracing::Span::current().record("total", total);
568 let served = total > 0;
569 Ok(RawRead {
570 start_offset: if served {
571 cursor.start
572 } else {
573 self.fetch_offset
574 },
575 bytes,
576 total,
577 last_offset: cursor.last.filter(|_| served),
578 })
579 }
580
581 /// Run the plan as [`Log::read_raw_desc`] would have read.
582 ///
583 /// # Errors
584 /// Returns an error when log I/O fails or a record or index is corrupt.
585 #[krabka_macros::sendfile_cfg]
586 pub fn read_regions(&self) -> Result<RawReadDesc, LogError> {
587 let lookback = cold_lookback(self.max_size);
588 // A segment whose run would be walked on bytes that are not in the
589 // page cache is not described (`read_raw_desc_if_cached`). The walk
590 // stops there, so the fetch sends the runs before it, or, when it
591 // starts in that segment, gets nothing and falls back to the
592 // byte-copy path, which reads the run in one large read. Passing over
593 // it would send a later segment's runs and skip its offsets.
594 let stopped = Cell::new(false);
595 let RawChunks {
596 cursor,
597 chunks: mut regions,
598 } = self.chunks(|view, next, remaining| {
599 if stopped.get() {
600 return Ok(None);
601 }
602 let Some(read) = view.read_raw_desc_if_cached(
603 next,
604 self.limit_offset,
605 remaining,
606 self.read_ahead_max,
607 )?
608 else {
609 stopped.set(true);
610 return Ok(None);
611 };
612 if let Some(region) = &read.region {
613 let end = region.offset.saturating_add(region.len as u64);
614 drop_cold(view, region.offset..end, lookback, false);
615 }
616 Ok(read.region.map(|region| RawChunk {
617 len: region.len,
618 data: region,
619 start: read.start_offset,
620 last: read.last_offset,
621 }))
622 })?;
623
624 if self.truncated_since() {
625 regions.clear();
626 }
627 let total: usize = regions.iter().map(|r| r.len).sum();
628 let span = tracing::Span::current();
629 span.record("regions", regions.len());
630 span.record("total", total);
631 Ok(RawReadDesc {
632 start_offset: if regions.is_empty() {
633 self.fetch_offset
634 } else {
635 cursor.start
636 },
637 regions,
638 total,
639 })
640 }
641
642 /// The walk both reads share: each view in turn, from the offset after
643 /// the one before it, while the budget and the limit allow and the one
644 /// before was read to its end.
645 fn chunks<T>(
646 &self,
647 mut read: impl FnMut(
648 &SegmentReadView,
649 Offset,
650 ByteSize,
651 ) -> Result<Option<RawChunk<T>>, LogError>,
652 ) -> Result<RawChunks<T>, LogError> {
653 let mut cursor = RawReadCursor::new(self.fetch_offset, self.max_size);
654 let mut chunks = Vec::new();
655 for view in &self.views {
656 if view.last() < cursor.next {
657 continue;
658 }
659 let Some(chunk) = read(view, cursor.next, cursor.budget())? else {
660 continue;
661 };
662 if !cursor.accept(chunk.start, chunk.last, chunk.len) {
663 break;
664 }
665 chunks.push(chunk.data);
666 if cursor.finished(view, self.limit_offset) {
667 break;
668 }
669 }
670 Ok(RawChunks { cursor, chunks })
671 }
672}
673
674/// Drop from the page cache what a fetch of `range` of `view`'s `.log` file
675/// leaves there, when the range is cold: see [`Log::cold_before`]. A
676/// `sendfile` fetch sends its bytes after this returns, so it drops the pages
677/// behind it, where its reader's previous fetch was, back to `lookback` bytes
678/// before the range. A fetch that already `held` its bytes, having copied
679/// them, drops its own pages as well. Whole pages only, so the page a
680/// `sendfile` range starts in stays for the send.
681///
682/// A consumer catching up on a backlog thus leaves at most a fetch or two of
683/// pages behind, instead of filling the cgroup to its limit and making every
684/// append reclaim: see [`page_cache`].
685#[cfg(not(target_os = "wasi"))]
686fn drop_cold(view: &SegmentReadView, range: Range<u64>, lookback: u64, held: bool) {
687 let Some(cold_before) = view.cold_before() else {
688 return;
689 };
690 if range.end > cold_before {
691 return;
692 }
693 let from = range.start.saturating_sub(lookback) & !(page_cache::PAGE - 1);
694 let to = if held {
695 range.end
696 } else {
697 range.start & !(page_cache::PAGE - 1)
698 };
699 if to > from {
700 view.drop_cached(from, to - from);
701 }
702}
703
704/// WASI has no flush pool, no page-cache ledger and no fetch that is cold.
705#[cfg(target_os = "wasi")]
706fn drop_cold(view: &SegmentReadView, range: Range<u64>, lookback: u64, held: bool) {
707 let _ = (view, range, lookback, held);
708}
709
710#[cfg(test)]
711mod tests;