Skip to main content

agave_fs/io_uring/
sequential_file_reader.rs

1#![allow(clippy::arithmetic_side_effects)]
2
3use {
4    super::{
5        IO_PRIO_BE_HIGHEST,
6        memory::{IoBufferChunk, PageAlignedMemory},
7    },
8    crate::{FileSize, IoSize, buffered_reader::FileBufRead, io_uring::sqpoll},
9    agave_io_uring::{Completion, Ring, RingAccess as _, RingOp},
10    io_uring::{IoUring, opcode, squeue, types},
11    std::{
12        collections::VecDeque,
13        fs::{File, OpenOptions},
14        io::{self, BufRead, Read},
15        marker::PhantomData,
16        mem,
17        ops::{Deref, DerefMut},
18        os::{
19            fd::{AsRawFd, BorrowedFd, RawFd},
20            unix::fs::OpenOptionsExt,
21        },
22        path::Path,
23        slice,
24    },
25};
26
27// Based on transfers seen with `dd bs=SIZE` for NVME drives: values >=64KiB are fine,
28// but peak at 1MiB. Also compare with particular NVME parameters, e.g.
29// 32 pages (Maximum Data Transfer Size) * page size (MPSMIN = Memory Page Size) = 128KiB.
30const DEFAULT_READ_SIZE: IoSize = 1024 * 1024;
31// For large file we don't really use workers as few regularly submitted requests get handled
32// within sqpoll thread. Allow some workers just in case, but limit them.
33const DEFAULT_MAX_IOWQ_WORKERS: u32 = 2;
34// This is conservative read size alignment for use with direct IO, some block devices may have
35// relaxed requirements, but detecting it is not trivial.
36const DIRECT_IO_READ_LEN_ALIGNMENT: IoSize = 4096;
37
38/// Utility for building `SequentialFileReader` with specified tuning options.
39pub struct SequentialFileReaderBuilder<'sp> {
40    read_capacity: IoSize,
41    max_iowq_workers: u32,
42    ring_squeue_size: Option<u32>,
43    shared_sqpoll_fd: Option<BorrowedFd<'sp>>,
44    /// Register buffer as fixed with the kernel
45    register_buffer: bool,
46    /// Toggle option for opening files with the O_DIRECT flag
47    use_direct_io: bool,
48}
49
50impl<'sp> SequentialFileReaderBuilder<'sp> {
51    pub fn new() -> Self {
52        Self {
53            read_capacity: DEFAULT_READ_SIZE,
54            max_iowq_workers: DEFAULT_MAX_IOWQ_WORKERS,
55            ring_squeue_size: None,
56            shared_sqpoll_fd: None,
57            register_buffer: false,
58            use_direct_io: false,
59        }
60    }
61
62    /// Override the default size of a single IO read operation
63    ///
64    /// This influences the concurrency, since buffer is divided into chunks of this size.
65    #[cfg(test)]
66    pub fn read_capacity(mut self, read_capacity: IoSize) -> Self {
67        self.read_capacity = read_capacity;
68        self
69    }
70
71    /// Set whether to register buffer with `io_uring` for improved performance.
72    ///
73    /// Enabling requires available memlock ulimit to be higher than sizes of registered buffers.
74    pub fn use_registered_buffers(mut self, register_buffers: bool) -> Self {
75        self.register_buffer = register_buffers;
76        self
77    }
78
79    /// Read files in direct-IO mode (disables kernel caching of read contents).
80    ///
81    /// Enabling requires the filesystem to support directio and `read_capacity`
82    /// to be a multiple of 4096.
83    pub fn use_direct_io(mut self, use_direct_io: bool) -> Self {
84        self.use_direct_io = use_direct_io;
85        self
86    }
87
88    /// Use (or remove) a shared kernel thread to drain submission queue for IO operations
89    pub fn shared_sqpoll(mut self, shared_sqpoll_fd: Option<BorrowedFd<'sp>>) -> Self {
90        self.shared_sqpoll_fd = shared_sqpoll_fd;
91        self
92    }
93
94    /// Build a new `SequentialFileReader` with internally allocated buffer.
95    ///
96    /// Buffer will hold at least `buf_capacity` bytes (increased to `read_capacity` if it's lower).
97    ///
98    /// Initially the reader is idle and starts reading after `set_file` is called. It will then execute
99    /// multiple `read_capacity` sized reads in parallel to fill the buffer.
100    pub fn build<'a>(self, buf_capacity: usize) -> io::Result<SequentialFileReader<'a>> {
101        let buf_capacity = buf_capacity.max(self.read_capacity as usize);
102        let buffer = PageAlignedMemory::new(buf_capacity)?;
103        self.build_with_buffer(buffer)
104    }
105
106    /// Build a new `SequentialFileReader` with a user-supplied buffer
107    ///
108    /// `buffer` is the internal buffer used for reading. It must be at least `read_capacity` long.
109    ///
110    /// Initially the reader is idle and starts reading after the first file is added.
111    /// The reader will execute multiple `read_capacity` sized reads in parallel to fill the buffer.
112    fn build_with_buffer<'a>(
113        self,
114        mut buffer: PageAlignedMemory,
115    ) -> io::Result<SequentialFileReader<'a>> {
116        // Align buffer capacity to read capacity, so we always read equally sized chunks
117        let buf_capacity =
118            buffer.as_mut().len() / self.read_capacity as usize * self.read_capacity as usize;
119        assert_ne!(buf_capacity, 0, "read size aligned buffer is too small");
120        let buf_slice_mut = &mut buffer.as_mut()[..buf_capacity];
121
122        // Safety: buffers contain unsafe pointers to `buffer`, but we make sure they are
123        // dropped before `backing_buffer` in `SequentialFileReader` is dropped.
124        let buffers = unsafe {
125            IoBufferChunk::split_buffer_chunks(
126                buf_slice_mut,
127                self.read_capacity,
128                self.register_buffer,
129            )
130        }
131        .map(ReadBufState::Uninit)
132        .collect();
133
134        let buffers_state = BuffersState(buffers);
135
136        let io_uring = self.create_io_uring(buf_capacity)?;
137        let ring = Ring::new(io_uring, buffers_state);
138
139        if self.register_buffer {
140            // Safety: kernel holds unsafe pointers to `buffer`, struct field declaration order
141            // guarantees that the ring is destroyed before `backing_buffer` is dropped.
142            unsafe { IoBufferChunk::register(buf_slice_mut, &ring)? };
143        }
144
145        if self.use_direct_io {
146            // O_DIRECT reads have size and alignment restrictions and must be into a sub-buffer of
147            // some multiple of the fs block size (see https://man7.org/linux/man-pages/man2/open.2.html#NOTES).
148            assert!(
149                self.read_capacity
150                    .is_multiple_of(DIRECT_IO_READ_LEN_ALIGNMENT),
151                "read size is not aligned for direct IO({} is not a multiple of \
152                 {DIRECT_IO_READ_LEN_ALIGNMENT})",
153                self.read_capacity
154            );
155        }
156
157        let open_file_flags = libc::O_NOATIME
158            | if self.use_direct_io {
159                libc::O_DIRECT
160            } else {
161                0
162            };
163
164        Ok(SequentialFileReader {
165            ring,
166            state: SequentialFileReaderState::default(),
167            open_file_flags,
168            backing_buffer: buffer,
169            _phantom: PhantomData,
170        })
171    }
172
173    fn create_io_uring(&self, buf_capacity: usize) -> io::Result<IoUring> {
174        // Let all buffers be submitted for reading at any time
175        let max_inflight_ops = (buf_capacity / self.read_capacity as usize) as u32;
176
177        // Completions arrive in bursts (batching done by the disk controller and the kernel).
178        // By submitting smaller chunks we decrease the likelihood that we stall on a full completion queue.
179        // Also, in order to keep some operations submitted at all times, we will `submit` them half-way
180        // through the buffer (at the cost of doubling syscalls) to let kernel work on one half while the other
181        // half is read by the user.
182        let ring_squeue_size = self
183            .ring_squeue_size
184            .unwrap_or((max_inflight_ops / 2).max(1));
185        // agave io_uring uses cqsize to define state slab size, so cqsize == max inflight ops
186        let ring = sqpoll::io_uring_builder_with(self.shared_sqpoll_fd)
187            .setup_cqsize(max_inflight_ops)
188            .build(ring_squeue_size)?;
189
190        // Maximum number of spawned [bounded IO, unbounded IO] kernel threads, we don't expect
191        // any unbounded work, but limit it to 1 just in case (0 leaves it unlimited).
192        ring.submitter()
193            .register_iowq_max_workers(&mut [self.max_iowq_workers, 1])?;
194        Ok(ring)
195    }
196}
197
198/// Reader for non-seekable files.
199///
200/// Implements read-ahead using io_uring.
201pub struct SequentialFileReader<'a> {
202    // Note: ring's state is tied to `backing_buffer` - contains unsafe pointer references
203    // to the buffer. Ring should be drained and dropped before `backing_buffer`.
204    ring: Ring<BuffersState, ReadOp>,
205    open_file_flags: i32,
206    state: SequentialFileReaderState,
207    /// Owned buffer used (chunked into `FixedIoBuffer` items) across lifespan of `inner`
208    /// (should get dropped last)
209    backing_buffer: PageAlignedMemory,
210    _phantom: PhantomData<&'a ()>,
211}
212
213impl<'a> SequentialFileReader<'a> {
214    /// Open file under `path`, check its metadata to determine read limit and add it to the reader.
215    ///
216    /// See `add_owned_file_to_prefetch` for more details.
217    pub fn set_path(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
218        let file = OpenOptions::new()
219            .read(true)
220            .custom_flags(self.open_file_flags)
221            .open(path)?;
222        let file_size = file.metadata()?.len();
223        self.add_owned_file_to_prefetch(file, file_size)
224    }
225
226    /// Add `file` to read. Starts reading the file as soon as a buffer is available.
227    ///
228    /// This function uses the direct io settings set in `SequentialFileReaderBuilder`.
229    ///
230    /// A direct io mode reader is safe to use with non direct io files. However, passing
231    /// direct io mode files to the reader in non direct io mode might result in an io error
232    /// due to unaligned read.
233    ///
234    /// It is up to the end user to ensure that they are passing files that conform to
235    /// the direct io settings of this `SequentialFileReader`.
236    ///
237    /// The read finishes when EOF is reached or `read_limit` bytes are read.
238    /// The `read_limit` must be less than or equal to the file size when in direct io mode.
239    /// Multiple files can be added to the reader and they will be read-ahead in FIFO order.
240    ///
241    /// Reader takes ownership of the file and will drop it after it's done reading
242    /// and `move_to_next_file` is called.
243    pub fn add_owned_file_to_prefetch(
244        &mut self,
245        file: File,
246        read_limit: FileSize,
247    ) -> io::Result<()> {
248        self.add_file_by_fd(file.as_raw_fd(), read_limit)?;
249        self.state.owned_files.push_back(file);
250        Ok(())
251    }
252
253    /// Reset to idle state and re-type with a fresh lifetime `'b`.
254    ///
255    /// Drains the prefetch queue (cancels in-flight reads) before returning.
256    pub fn rebind<'b>(mut self) -> io::Result<SequentialFileReader<'b>> {
257        while !self.state.files.is_empty() {
258            self.move_to_next_file()?;
259        }
260        Ok(SequentialFileReader {
261            ring: self.ring,
262            open_file_flags: self.open_file_flags,
263            state: self.state,
264            backing_buffer: self.backing_buffer,
265            _phantom: PhantomData,
266        })
267    }
268
269    /// Caller must ensure that the file is not closed while the reader is using it.
270    fn add_file_by_fd(&mut self, fd: RawFd, read_limit: FileSize) -> io::Result<()> {
271        // Use `open_file_flags` to set the `is_direct_io` parameter
272        self.state.files.push_back(FileState::new(
273            fd,
274            self.open_file_flags & libc::O_DIRECT == libc::O_DIRECT,
275            read_limit,
276        ));
277
278        if self.state.all_buffers_used(self.ring.context()) {
279            // Just added file to backlog, no reads can be started yet.
280            return Ok(());
281        }
282
283        // There are free buffers, so we can start reading the new file.
284        self.state.next_read_file_index =
285            Some(self.state.next_read_file_index.map_or(0, |idx| idx + 1));
286
287        // Start reading as many buffers as necessary for queued files.
288        self.try_schedule_new_ops()
289    }
290
291    /// When reading multiple files, this method moves the reader to the next file.
292    fn move_to_next_file(&mut self) -> io::Result<()> {
293        let state = &mut self.state;
294
295        let Some(removed_file) = state.files.pop_front() else {
296            return Ok(());
297        };
298
299        // Always reset in-file and in-buffer state
300        state.current_offset = 0;
301        state.current_buf_pos = 0;
302        state.current_buf_remaining = 0;
303        state.left_to_consume = 0;
304
305        if removed_file.had_scheduled_reads() {
306            // Reclaim current and all subsequent unread buffers of removed file as uninitialized.
307            // This includes all buffers until sentinel index, which is:
308            // * an index used for next scheduled read (if any file has some scheduled)
309            // * otherwise `state.next_read_buf_index` (default buffer index to start read from)
310            let sentinel_buf_index = state
311                .files
312                .iter()
313                .find_map(|f| f.start_buf_index)
314                .unwrap_or(state.next_read_buf_index);
315            let num_bufs = self.ring.context().len();
316            loop {
317                self.ring.process_completions()?;
318                let current_buf = self.ring.context_mut().get_mut(state.current_buf_index);
319                if current_buf.is_reading() {
320                    // Still no data, wait for more completions, but submit in case there are queued
321                    // entries in the submission queue.
322                    self.ring.submit()?;
323                    continue;
324                }
325                current_buf.transition_to_uninit();
326
327                let next_buf_index = (state.current_buf_index + 1) % num_bufs;
328                state.current_buf_index = next_buf_index;
329                if sentinel_buf_index == next_buf_index {
330                    break;
331                }
332            }
333        }
334
335        if state
336            .owned_files
337            .front()
338            .is_some_and(|f| removed_file.is_same_file(f))
339        {
340            state.owned_files.pop_front();
341        }
342
343        if let Some(next_file_index) = state.next_read_file_index.as_mut() {
344            // Since file was removed from front, all indices are shifted by one
345            state.next_read_file_index = next_file_index.checked_sub(1);
346            if state.next_read_file_index.is_none() {
347                // The removed file was the current one being read
348                if state.files.is_empty() {
349                    // Reader is empty, reset buf indices to initial values
350                    state.current_buf_index = 0;
351                    state.next_read_buf_index = 0;
352                } else {
353                    // There are other files to read, start with the new first file
354                    state.next_read_file_index = Some(0);
355                }
356            }
357        }
358
359        self.try_schedule_new_ops()
360    }
361
362    fn try_schedule_new_ops(&mut self) -> io::Result<()> {
363        // Start reading as many buffers as necessary for queued files.
364        while let Some(op) = self.state.next_read_op(self.ring.context_mut()) {
365            self.ring.push(op)?;
366        }
367        Ok(())
368    }
369
370    fn wait_current_buf_full(&mut self) -> io::Result<bool> {
371        if self
372            .state
373            .files
374            .front()
375            .is_none_or(|file| !file.had_scheduled_reads())
376        {
377            return Ok(false);
378        }
379        let num_bufs = self.ring.context().len();
380        loop {
381            self.ring.process_completions()?;
382
383            let state = &mut self.state;
384            let current_buf = &mut self.ring.context_mut().get_mut(state.current_buf_index);
385            match current_buf {
386                ReadBufState::Full { buf, eof_pos } => {
387                    if state.current_buf_remaining == 0 && state.current_buf_pos == 0 {
388                        // Initialize consuming new buffer.
389                        state.current_buf_remaining = eof_pos.unwrap_or(buf.len());
390                        if state.left_to_consume > 0 {
391                            // Skip any bytes remaining from previous unfulfilled consumes.
392                            let consumed = state
393                                .left_to_consume
394                                .min(state.current_buf_remaining as usize);
395                            state.left_to_consume -= consumed;
396                            state.current_buf_pos += consumed as IoSize;
397                            state.current_buf_remaining -= consumed as IoSize;
398                        }
399                    }
400
401                    // Note: we might have consumed whole buf from `left_to_consume`
402                    if state.current_buf_remaining > 0 {
403                        // We have some data available.
404                        return Ok(true);
405                    }
406
407                    if eof_pos.is_some() {
408                        // Last filled buf for the whole file (until `move_to_next_file` is called).
409                        return Ok(false);
410                    }
411                    // We have finished consuming this buffer - reset its state.
412                    current_buf.transition_to_uninit();
413
414                    // Next `fill_buf` will use subsequent buffer.
415                    state.move_to_next_buf(num_bufs);
416
417                    // A buffer was freed, so try to queue up next read.
418                    self.try_schedule_new_ops()?;
419                }
420
421                ReadBufState::Reading => {
422                    // Still no data, wait for more completions, but submit in case there are queued
423                    // entries in the submission queue.
424                    self.ring.submit()?
425                }
426
427                ReadBufState::Uninit(_) => unreachable!("should be initialized"),
428            }
429            // Move to the next buffer and check again whether we have data.
430        }
431    }
432}
433
434// BufRead requires Read, but we never really use the Read interface.
435impl<'a> Read for SequentialFileReader<'a> {
436    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
437        let available = self.fill_buf()?;
438        let bytes_to_read = available.len().min(buf.len());
439        if bytes_to_read == 0 {
440            return Ok(0); // EOF or empty `buf`
441        }
442        buf[..bytes_to_read].copy_from_slice(&available[..bytes_to_read]);
443        self.state.consume_in_current_buf(bytes_to_read);
444        Ok(bytes_to_read)
445    }
446
447    #[inline]
448    fn read_exact(&mut self, mut buf: &mut [u8]) -> io::Result<()> {
449        while !buf.is_empty() {
450            if self.state.current_buf_remaining == 0 && !self.wait_current_buf_full()? {
451                return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "read_exact"));
452            }
453            let current_buf = self.ring.context().get_fast(self.state.current_buf_index);
454            let to_copy_len = buf.len().min(self.state.current_buf_remaining as usize);
455            let slice = current_buf.slice(self.state.current_buf_pos, to_copy_len as IoSize);
456            self.state.consume_in_current_buf(to_copy_len);
457            // Safety: to_copy_len is at most buf.len() checked with `min` above
458            let (to_fill, remaining) = unsafe { buf.split_at_mut_unchecked(to_copy_len) };
459            to_fill.copy_from_slice(slice);
460            buf = remaining;
461        }
462        Ok(())
463    }
464}
465
466impl<'a> BufRead for SequentialFileReader<'a> {
467    fn fill_buf(&mut self) -> io::Result<&[u8]> {
468        if self.state.current_buf_remaining == 0 && !self.wait_current_buf_full()? {
469            return Ok(&[]);
470        }
471
472        // At this point we must have data or be at EOF.
473        let current_buf = self.ring.context().get_fast(self.state.current_buf_index);
474        Ok(current_buf.slice(self.state.current_buf_pos, self.state.current_buf_remaining))
475    }
476
477    #[inline]
478    fn consume(&mut self, amt: usize) {
479        self.state.consume_in_current_buf(amt);
480    }
481}
482
483impl<'a> FileBufRead<'a> for SequentialFileReader<'a> {
484    /// The `SequentialFileReader` must be in direct io mode if passing in direct io files.
485    /// `read_limit` must be less than the file size if using direct io.
486    /// See `add_owned_file_to_prefetch` for more details.
487    fn set_file(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> {
488        // Pop the front file while it's a different file, or while it's the same file
489        // but already partially consumed (so re-prefetching restarts at offset 0,
490        // honoring the trait contract).
491        while self.state.files.front().is_some_and(|file_state| {
492            !file_state.is_same_file(file)
493                || file_state.read_limit != read_limit
494                || self.state.current_offset > 0
495        }) {
496            self.move_to_next_file()?;
497        }
498        if self.state.files.is_empty() {
499            self.add_file_to_prefetch(file, read_limit)?;
500        }
501        Ok(())
502    }
503
504    fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> {
505        self.add_file_by_fd(file.as_raw_fd(), read_limit)
506    }
507
508    fn get_file_offset(&self) -> FileSize {
509        self.state.current_offset
510    }
511
512    fn consume_or_skip(&mut self, amt: usize) {
513        self.state.consume_or_skip(amt);
514    }
515}
516
517/// Holds the state of all the buffers that may be submitted to the kernel for reading.
518struct BuffersState(Box<[ReadBufState]>);
519
520impl BuffersState {
521    fn len(&self) -> u16 {
522        self.0.len() as u16
523    }
524
525    fn get_mut(&mut self, index: u16) -> &mut ReadBufState {
526        &mut self.0[index as usize]
527    }
528
529    #[inline]
530    fn get_fast(&self, index: u16) -> &ReadBufState {
531        debug_assert!(index < self.len());
532        // Perf: skip bounds check for performance
533        unsafe { self.0.get_unchecked(index as usize) }
534    }
535}
536
537impl Deref for BuffersState {
538    type Target = [ReadBufState];
539
540    fn deref(&self) -> &Self::Target {
541        &self.0
542    }
543}
544
545impl DerefMut for BuffersState {
546    fn deref_mut(&mut self) -> &mut Self::Target {
547        &mut self.0
548    }
549}
550
551/// Holds the state of the reader.
552#[derive(Debug, Default)]
553struct SequentialFileReaderState {
554    // Note: file states operate on file descriptors of files that are assumed to be open,
555    // which is guaranteed either by them being in `owned_files` or in case of file references
556    // because they are added with reader's 'a lifetime.
557    files: VecDeque<FileState>,
558
559    /// Amount of bytes left to consume from next buffer(s) before returning them in `fill_buf()`.
560    /// This is necessary to handle `consume()` calls beyond the current buffer.
561    left_to_consume: usize,
562    /// Index of `BuffersState` buffer to consume data from (0 if no file is being read)
563    current_buf_index: u16,
564    /// Position in buffer (pointed by `current_buf_index`) to consume data from
565    current_buf_pos: IoSize,
566    /// Remaining bytes in the current buffer (0 until `wait_current_buf_full` initializes it)
567    current_buf_remaining: IoSize,
568    /// File offset of the next `fill_buf()` buffer available to consume
569    current_offset: FileSize,
570
571    /// Index in `self.files` of the file that is currently being read (can generate new read ops).
572    next_read_file_index: Option<usize>,
573    /// Index of `BuffersState` buffer that can be used for the next read operation.
574    next_read_buf_index: u16,
575
576    owned_files: VecDeque<File>,
577}
578
579impl SequentialFileReaderState {
580    #[inline]
581    fn consume_in_current_buf(&mut self, amt: usize) {
582        self.current_offset += amt as FileSize;
583        self.current_buf_pos += amt as IoSize;
584        self.current_buf_remaining -= amt as IoSize;
585    }
586
587    fn consume_or_skip(&mut self, amt: usize) {
588        if amt == 0 || self.files.is_empty() {
589            return;
590        }
591        self.current_offset += amt as FileSize;
592
593        let unconsumed_buf_len = self.current_buf_remaining as usize;
594        if let Some(new_remaining) = unconsumed_buf_len.checked_sub(amt) {
595            self.current_buf_pos += amt as IoSize;
596            self.current_buf_remaining = new_remaining as IoSize;
597        } else {
598            self.current_buf_pos += self.current_buf_remaining;
599            self.current_buf_remaining = 0;
600            // Keep track of any bytes left to consume beyond current buffer, they will be
601            // accounted for during next `wait_current_buf_full` call.
602            self.left_to_consume += amt - unconsumed_buf_len;
603        }
604    }
605
606    /// Return the next read operation for the reader.
607    ///
608    /// If all buffers are used or last file is already (being) read, returns `None`.
609    ///
610    /// Reads are issued for files added into the reader from first file at position 0
611    /// to its limit / EOF and then for any subsequent files.
612    fn next_read_op(&mut self, bufs: &mut [ReadBufState]) -> Option<ReadOp> {
613        if self.all_buffers_used(bufs) {
614            return None;
615        }
616        let num_bufs = bufs.len() as u16;
617        loop {
618            let read_file_index = self.next_read_file_index?;
619            match self.files[read_file_index].next_read_op(self.next_read_buf_index, bufs) {
620                Some(op) => {
621                    self.next_read_buf_index = (self.next_read_buf_index + 1) % num_bufs;
622                    return Some(op);
623                }
624                None => {
625                    // Last read file reached its limit, try to move to the next file
626                    if read_file_index < self.files.len() - 1 {
627                        self.next_read_file_index = Some(read_file_index + 1);
628                    } else {
629                        return None;
630                    }
631                }
632            }
633        }
634    }
635
636    fn move_to_next_buf(&mut self, num_bufs: u16) {
637        self.current_buf_index = (self.current_buf_index + 1) % num_bufs;
638        self.current_buf_pos = 0;
639        // Buffer might still be reading, len will be intialized on first `wait_current_buf_full`
640        self.current_buf_remaining = 0;
641    }
642
643    /// Returns `true` if there are no more buffers available for reading.
644    fn all_buffers_used(&self, bufs: &[ReadBufState]) -> bool {
645        bufs[self.next_read_buf_index as usize].is_used()
646    }
647}
648
649/// Holds the state of a single file being read.
650#[derive(Debug)]
651struct FileState {
652    raw_fd: RawFd,
653    /// Is the file opened with direct io
654    is_direct_io: bool,
655    /// Limit file offset to read up to.
656    read_limit: FileSize,
657    /// Offset of the next byte to read from the file
658    next_read_offset: FileSize,
659    /// When the file is possible to read for the first time, it should be read from this buffer index
660    start_buf_index: Option<u16>,
661}
662
663impl FileState {
664    fn new(raw_fd: RawFd, is_direct_io: bool, read_limit: FileSize) -> Self {
665        Self {
666            raw_fd,
667            is_direct_io,
668            read_limit,
669            next_read_offset: 0,
670            start_buf_index: None,
671        }
672    }
673
674    fn is_same_file(&self, file: &File) -> bool {
675        self.raw_fd == file.as_raw_fd()
676    }
677
678    fn had_scheduled_reads(&self) -> bool {
679        self.start_buf_index.is_some()
680    }
681
682    /// Create a new read operation into the `bufs[index]` buffer and update file state.
683    ///
684    /// This is called whenever new reads can be scheduled (on added file or freed buffer).
685    ///
686    /// Returns `ReadOp` that will read
687    /// [self.next_read_offset, self.next_read_offset + min(buf len, self.read_limit))
688    /// from the file into `bufs[index]`. Once the read is complete the buffer changes into
689    /// `Full` state and can be consumed.
690    fn next_read_op(&mut self, index: u16, bufs: &mut [ReadBufState]) -> Option<ReadOp> {
691        let Self {
692            start_buf_index,
693            raw_fd,
694            is_direct_io,
695            next_read_offset: offset,
696            read_limit,
697        } = self;
698        let left_to_read = read_limit.saturating_sub(*offset);
699        if left_to_read == 0 {
700            return None;
701        }
702
703        let buf = bufs[index as usize].transition_to_reading();
704
705        let read_len = left_to_read.min(buf.len() as FileSize);
706        let op = ReadOp {
707            fd: types::Fd(*raw_fd),
708            buf,
709            is_direct_io: *is_direct_io,
710            buf_offset: 0,
711            file_offset: *offset,
712            read_len: read_len as u32, // it's trimmed by u32 buf.len() above
713            is_last_read: left_to_read == read_len,
714            reader_buf_index: index,
715        };
716        // Mark file state to start reading at `index` buffer
717        if start_buf_index.is_none() {
718            *start_buf_index = Some(index);
719        }
720
721        // We always advance by `read_len`. If we get a short read, we submit a new
722        // read for the remaining data. See ReadOp::complete().
723        *offset += read_len;
724
725        Some(op)
726    }
727}
728
729/// Tracks usage stages of single `IoBufferChunk` as it goes through io-uring operation
730#[derive(Debug)]
731enum ReadBufState {
732    /// The buffer is pending submission to read queue (on initialization and
733    /// in transition from `Full` to `Reading`).
734    Uninit(IoBufferChunk),
735    /// The buffer is currently being read and there's a corresponding ReadOp in
736    /// the ring.
737    Reading,
738    /// The buffer is filled and ready to be consumed.
739    Full {
740        buf: IoBufferChunk,
741        /// Position in `buf` at which 0-sized read (or requested read limit) was reached
742        eof_pos: Option<u32>,
743    },
744}
745
746impl ReadBufState {
747    fn is_used(&self) -> bool {
748        matches!(self, ReadBufState::Reading | ReadBufState::Full { .. })
749    }
750
751    fn is_reading(&self) -> bool {
752        matches!(self, ReadBufState::Reading)
753    }
754
755    #[inline]
756    fn slice(&self, start_pos: IoSize, len: IoSize) -> &[u8] {
757        match self {
758            Self::Full { buf, eof_pos } => {
759                debug_assert!(eof_pos.unwrap_or(buf.len()) >= start_pos + len);
760                // Safety: `limit` is at most `buf.len() - start_pos` (as asserted for `end_pos`),
761                // so the slice is valid given buffer's validity
762                unsafe { slice::from_raw_parts(buf.as_ptr().add(start_pos as usize), len as usize) }
763            }
764            Self::Uninit(_) | Self::Reading => {
765                unreachable!("must call as_slice only on full buffer")
766            }
767        }
768    }
769
770    /// Marks the buffer as uninitialized (after it has been fully consumed).
771    fn transition_to_uninit(&mut self) {
772        match self {
773            Self::Uninit(_) => (),
774            Self::Reading => unreachable!("cannot reset a buffer that has pending read"),
775            Self::Full { buf, .. } => {
776                *self = ReadBufState::Uninit(mem::replace(buf, IoBufferChunk::empty()));
777            }
778        }
779    }
780
781    /// Marks the buffer as being read and returns underlying buffer to pass to `ReadOp`.
782    #[must_use]
783    fn transition_to_reading(&mut self) -> IoBufferChunk {
784        let Self::Uninit(buf) = mem::replace(self, Self::Reading) else {
785            unreachable!("buffer should be uninitialized")
786        };
787        buf
788    }
789}
790
791#[derive(Debug)]
792struct ReadOp {
793    fd: types::Fd,
794    buf: IoBufferChunk,
795    is_direct_io: bool,
796    /// This is the offset inside the buffer. It's typically 0, but can be non-zero if a previous
797    /// read returned less data than requested (because of EINTR or whatever) and we submitted a new
798    /// read for the remaining data.
799    buf_offset: IoSize,
800    /// The offset in the file.
801    file_offset: FileSize,
802    /// The length of the read. This is typically `read_capacity` but can be less if a previous read
803    /// returned less data than requested or `file_offset` is close to the end of read limit.
804    read_len: IoSize,
805    /// Indicates that after reading `read_len` we have reached configured read limit.
806    is_last_read: bool,
807    /// This is the index of the buffer in the reader's state. It's used to update the state once the
808    /// read completes.
809    reader_buf_index: u16,
810}
811
812impl RingOp<BuffersState> for ReadOp {
813    fn entry(&mut self) -> squeue::Entry {
814        let ReadOp {
815            fd,
816            buf,
817            is_direct_io,
818            buf_offset,
819            file_offset,
820            read_len,
821            is_last_read: _,
822            reader_buf_index: _,
823        } = self;
824
825        // Align the read length if necessary
826        let internal_read_len = if *is_direct_io && *read_len != buf.len() {
827            // Try to align the read len if possible and fall back to reading
828            // the full remaining bytes if we can't align the read len.
829            read_len
830                .next_multiple_of(DIRECT_IO_READ_LEN_ALIGNMENT)
831                .min(buf.len() - *buf_offset)
832        } else {
833            *read_len
834        };
835        debug_assert!(*buf_offset + internal_read_len <= buf.len());
836        // Safety: we assert that the buffer is large enough to hold the read.
837        let buf_ptr = unsafe { buf.as_mut_ptr().byte_add(*buf_offset as usize) };
838
839        let entry = match buf.io_buf_index() {
840            Some(io_buf_index) => {
841                opcode::ReadFixed::new(*fd, buf_ptr, internal_read_len, io_buf_index)
842                    .offset(*file_offset)
843                    .ioprio(IO_PRIO_BE_HIGHEST)
844                    .build()
845            }
846            None => opcode::Read::new(*fd, buf_ptr, internal_read_len)
847                .offset(*file_offset)
848                .ioprio(IO_PRIO_BE_HIGHEST)
849                .build(),
850        };
851        entry.flags(squeue::Flags::ASYNC)
852    }
853
854    fn complete(
855        &mut self,
856        completion: &mut Completion<BuffersState, Self>,
857        res: io::Result<i32>,
858    ) -> io::Result<()> {
859        let ReadOp {
860            fd,
861            buf,
862            is_direct_io,
863            buf_offset,
864            file_offset,
865            read_len,
866            is_last_read,
867            reader_buf_index,
868        } = self;
869        let buffers = completion.context_mut();
870
871        let last_read_len = res? as IoSize;
872
873        let total_read_len = *buf_offset + last_read_len;
874        let buf = mem::replace(buf, IoBufferChunk::empty());
875
876        if last_read_len > 0 && last_read_len < *read_len {
877            // Partial read, retry the op with updated offsets
878            let op: ReadOp = ReadOp {
879                fd: *fd,
880                buf,
881                is_direct_io: *is_direct_io,
882                buf_offset: total_read_len,
883                file_offset: *file_offset + last_read_len as FileSize,
884                read_len: *read_len - last_read_len,
885                reader_buf_index: *reader_buf_index,
886                is_last_read: *is_last_read,
887            };
888            // Safety:
889            // The op points to a buffer which is guaranteed to be valid for the
890            // lifetime of the operation
891            completion.push(op)?;
892        } else {
893            buffers[*reader_buf_index as usize] = ReadBufState::Full {
894                buf,
895                eof_pos: (last_read_len == 0 || *is_last_read).then_some(total_read_len),
896            };
897        }
898
899        Ok(())
900    }
901}
902
903#[cfg(test)]
904mod tests {
905    use {super::*, std::io::Seek, tempfile::NamedTempFile};
906
907    fn write_test_pattern(num_bytes: usize, dst: &mut impl io::Write) -> Vec<u8> {
908        let pattern = (0..num_bytes).map(|i| i as u8).collect::<Vec<_>>();
909        io::Write::write_all(dst, &pattern).expect("must write prepared pattern");
910        pattern
911    }
912
913    fn read_as_vec(mut reader: impl Read) -> Vec<u8> {
914        let mut buf = Vec::new();
915        reader.read_to_end(&mut buf).unwrap();
916        buf
917    }
918
919    fn check_reading_file(
920        file_size: FileSize,
921        backing_buffer_size: usize,
922        read_capacity: IoSize,
923        use_direct_io: bool,
924    ) {
925        let pattern: Vec<u8> = (0..251).collect();
926
927        // Create a temp file and write the pattern to it repeatedly
928        let mut temp_file = NamedTempFile::new().unwrap();
929        for _ in 0..file_size as usize / pattern.len() {
930            io::Write::write_all(&mut temp_file, &pattern).unwrap();
931        }
932        io::Write::write_all(
933            &mut temp_file,
934            &pattern[..file_size as usize % pattern.len()],
935        )
936        .unwrap();
937
938        let buf = PageAlignedMemory::new(backing_buffer_size).unwrap();
939        let mut reader = SequentialFileReaderBuilder::new()
940            .use_direct_io(use_direct_io)
941            .read_capacity(read_capacity)
942            .build_with_buffer(buf)
943            .unwrap();
944        reader.set_path(temp_file.path()).unwrap();
945
946        // Read contents from the reader and verify length
947        let all_read_data = read_as_vec(&mut reader);
948        assert_eq!(all_read_data.len() as FileSize, file_size);
949        assert_eq!(reader.get_file_offset(), file_size);
950
951        // Verify the contents
952        for (i, byte) in all_read_data.iter().enumerate() {
953            assert_eq!(*byte, pattern[i % pattern.len()], "Mismatch - pos {i}");
954        }
955    }
956
957    #[test]
958    fn test_reading_empty_file() {
959        check_reading_file(0, 4096, 1024, false);
960    }
961
962    /// Test with buffer larger than the whole file
963    #[test]
964    fn test_reading_small_file() {
965        check_reading_file(2500, 4096, 1024, false);
966        check_reading_file(2500, 4096, 2048, false);
967        check_reading_file(2500, 4096, 4096, false);
968    }
969
970    /// Test with buffer smaller than the whole file
971    #[test]
972    fn test_reading_file_in_chunks() {
973        check_reading_file(25_000, 16384, 1024, false);
974        check_reading_file(25_000, 4096, 1024, false);
975        check_reading_file(25_000, 4096, 2048, false);
976        check_reading_file(25_000, 4096, 4096, false);
977    }
978
979    /// Test with buffer much smaller than the whole file
980    #[test]
981    fn test_reading_large_file() {
982        check_reading_file(250_000, 32768, 1024, false);
983        check_reading_file(250_000, 16384, 1024, false);
984        check_reading_file(250_000, 4096, 1024, false);
985        check_reading_file(250_000, 4096, 2048, false);
986        check_reading_file(250_000, 4096, 4096, false);
987    }
988
989    #[test]
990    fn test_non_registered_buffer_read() {
991        let file_size = 64 * 1024;
992        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
993        let data = write_test_pattern(file_size, &mut temp_file);
994
995        let mut reader = SequentialFileReaderBuilder::new()
996            .read_capacity(4 * 1024)
997            .use_registered_buffers(false)
998            .build(16 * 1024)
999            .unwrap();
1000        reader.set_path(temp_file.path()).unwrap();
1001
1002        let mut all_read_data = Vec::new();
1003        reader.read_to_end(&mut all_read_data).unwrap();
1004        assert_eq!(all_read_data.len(), file_size);
1005        assert_eq!(all_read_data, data);
1006    }
1007
1008    #[test]
1009    fn test_add_file_ref() {
1010        let mut temp_file = NamedTempFile::new().unwrap();
1011        io::Write::write_all(&mut temp_file, &[0xa, 0xb, 0xc]).unwrap();
1012        temp_file.rewind().unwrap();
1013
1014        {
1015            let mut reader = SequentialFileReaderBuilder::new()
1016                .read_capacity(512)
1017                .build(1024)
1018                .unwrap();
1019            reader.add_file_to_prefetch(temp_file.as_file(), 3).unwrap();
1020            assert_eq!(read_as_vec(&mut reader), &[0xa, 0xb, 0xc]);
1021        }
1022        // Independently we can also read from the file directly
1023        assert_eq!(read_as_vec(&mut temp_file), &[0xa, 0xb, 0xc]);
1024    }
1025
1026    #[test]
1027    fn test_direct_io_read() {
1028        check_reading_file(0, 4096, 4096, true);
1029        check_reading_file(2_500, 4096, 4096, true);
1030        check_reading_file(2_500, 16384, 4096, true);
1031        check_reading_file(25_000, 4096, 4096, true);
1032        check_reading_file(25_000, 16384, 4096, true);
1033        check_reading_file(250_000, 4096, 4096, true);
1034        check_reading_file(250_000, 16384, 4096, true);
1035        check_reading_file(4096, 4096, 4096, true);
1036        check_reading_file(4096, 16384, 4096, true);
1037        check_reading_file(16384, 4096, 4096, true);
1038        check_reading_file(16384, 16384, 4096, true);
1039    }
1040
1041    #[test]
1042    fn test_multiple_unlimited_files() {
1043        let mut temp1 = NamedTempFile::new().unwrap();
1044        io::Write::write_all(&mut temp1, &[0xa, 0xb, 0xc]).unwrap();
1045        let mut temp2 = NamedTempFile::new().unwrap();
1046        io::Write::write_all(&mut temp2, &[0xd, 0xe, 0xf, 0x10]).unwrap();
1047
1048        let mut reader = SequentialFileReaderBuilder::new()
1049            .read_capacity(512)
1050            .build(1024)
1051            .unwrap();
1052
1053        let f1 = File::open(temp1.path()).unwrap();
1054        let f2 = File::open(temp2.path()).unwrap();
1055        reader
1056            .add_owned_file_to_prefetch(f1, FileSize::MAX)
1057            .unwrap();
1058        reader
1059            .add_owned_file_to_prefetch(f2, FileSize::MAX)
1060            .unwrap();
1061
1062        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1063        reader.move_to_next_file().unwrap();
1064
1065        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf, 0x10]);
1066        reader.move_to_next_file().unwrap();
1067
1068        let f1 = File::open(temp1.path()).unwrap();
1069        reader
1070            .add_owned_file_to_prefetch(f1, FileSize::MAX)
1071            .unwrap();
1072        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1073    }
1074
1075    #[test]
1076    fn test_get_offset() {
1077        let mut temp1 = NamedTempFile::new().unwrap();
1078        write_test_pattern(600, &mut temp1);
1079
1080        let mut reader = SequentialFileReaderBuilder::new()
1081            .read_capacity(512)
1082            .build(1024)
1083            .unwrap();
1084        reader.add_file_to_prefetch(temp1.as_file(), 1990).unwrap();
1085
1086        assert_eq!(512, reader.fill_buf().unwrap().len());
1087        assert_eq!(0, reader.get_file_offset());
1088        reader.consume(0);
1089        assert_eq!(0, reader.get_file_offset());
1090
1091        reader.consume(40);
1092        assert_eq!(40, reader.get_file_offset());
1093        assert_eq!(472, reader.fill_buf().unwrap().len());
1094
1095        reader.consume(472);
1096        assert_eq!(512, reader.get_file_offset());
1097        assert_eq!(88, reader.fill_buf().unwrap().len());
1098        reader.consume(0);
1099        assert_eq!(512, reader.get_file_offset());
1100
1101        reader.consume(88);
1102        assert_eq!(600, reader.get_file_offset());
1103        assert_eq!(0, reader.fill_buf().unwrap().len());
1104
1105        reader.move_to_next_file().unwrap();
1106        assert_eq!(0, reader.get_file_offset());
1107    }
1108
1109    #[test]
1110    fn test_consume_skip_filled_buf_len() {
1111        let mut temp1 = NamedTempFile::new().unwrap();
1112        let pattern = write_test_pattern(6000, &mut temp1);
1113
1114        let mut reader = SequentialFileReaderBuilder::new()
1115            .read_capacity(512)
1116            .build(2048)
1117            .unwrap();
1118        reader.add_file_to_prefetch(temp1.as_file(), 5990).unwrap();
1119
1120        assert_eq!(reader.fill_buf().unwrap(), &pattern[..512]);
1121        assert_eq!(0, reader.get_file_offset());
1122
1123        reader.consume_or_skip(600);
1124        assert_eq!(600, reader.get_file_offset());
1125        assert_eq!(reader.fill_buf().unwrap(), &pattern[600..1024]);
1126
1127        reader.consume_or_skip(400);
1128        assert_eq!(1000, reader.get_file_offset());
1129        assert_eq!(reader.fill_buf().unwrap(), &pattern[1000..1024]);
1130
1131        reader.consume_or_skip(25);
1132        assert_eq!(reader.fill_buf().unwrap(), &pattern[1025..1536]);
1133
1134        reader.consume_or_skip(2000);
1135        assert_eq!(reader.fill_buf().unwrap(), &pattern[3025..3072]);
1136    }
1137
1138    #[test]
1139    fn test_set_file() {
1140        let mut temp1 = NamedTempFile::new().unwrap();
1141        io::Write::write_all(&mut temp1, &[0xa, 0xb, 0xc]).unwrap();
1142        let mut temp2 = NamedTempFile::new().unwrap();
1143        io::Write::write_all(&mut temp2, &[0xd, 0xe, 0xf, 0x10]).unwrap();
1144
1145        let mut reader = SequentialFileReaderBuilder::new()
1146            .read_capacity(512)
1147            .build(1024)
1148            .unwrap();
1149        reader.add_file_to_prefetch(temp1.as_file(), 3).unwrap();
1150        reader.add_file_to_prefetch(temp2.as_file(), 4).unwrap();
1151
1152        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1153
1154        reader.set_file(temp2.as_file(), 4).unwrap();
1155        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf, 0x10]);
1156
1157        reader.set_file(temp1.as_file(), 4).unwrap();
1158        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1159
1160        let f1 = File::open(temp1.path()).unwrap();
1161        reader
1162            .add_owned_file_to_prefetch(f1, FileSize::MAX)
1163            .unwrap();
1164        reader.move_to_next_file().unwrap();
1165        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1166
1167        reader.set_file(temp2.as_file(), 4).unwrap();
1168        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf, 0x10]);
1169
1170        // Re-setting the same file after consuming it must rewind to offset 0.
1171        reader.set_file(temp2.as_file(), 4).unwrap();
1172        assert_eq!(reader.get_file_offset(), 0);
1173        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf, 0x10]);
1174
1175        // Re-setting the same file at offset 0 but with a different `read_limit`
1176        // must also re-prefetch the file so the new limit takes effect.
1177        reader.set_file(temp2.as_file(), 2).unwrap();
1178        assert_eq!(reader.get_file_offset(), 0);
1179        // Only `read_limit` differs from the front file (offset is still 0):
1180        // this exercises the `read_limit` mismatch branch of `set_file`.
1181        reader.set_file(temp2.as_file(), 3).unwrap();
1182        assert_eq!(reader.get_file_offset(), 0);
1183        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf]);
1184    }
1185
1186    #[test]
1187    fn test_multiple_files_including_zero_limit() {
1188        let mut temp1 = NamedTempFile::new().unwrap();
1189        io::Write::write_all(&mut temp1, &[0xa, 0xb, 0xc]).unwrap();
1190        let mut temp2 = NamedTempFile::new().unwrap();
1191        io::Write::write_all(&mut temp2, &[0xd, 0xe, 0xf, 0x10]).unwrap();
1192
1193        let mut reader = SequentialFileReaderBuilder::new()
1194            .read_capacity(512)
1195            .build(1024)
1196            .unwrap();
1197
1198        reader.add_file_to_prefetch(temp1.as_file(), 3).unwrap();
1199        reader.add_file_to_prefetch(temp2.as_file(), 0).unwrap();
1200        reader.add_file_to_prefetch(temp1.as_file(), 10).unwrap();
1201
1202        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1203
1204        reader.move_to_next_file().unwrap();
1205        assert_eq!(read_as_vec(&mut reader), vec![]);
1206
1207        reader.move_to_next_file().unwrap();
1208        assert_eq!(read_as_vec(&mut reader), vec![0xa, 0xb, 0xc]);
1209
1210        reader.add_file_to_prefetch(temp1.as_file(), 0).unwrap();
1211        reader.move_to_next_file().unwrap();
1212        assert_eq!(read_as_vec(&mut reader), vec![]);
1213
1214        reader.add_file_to_prefetch(temp2.as_file(), 4).unwrap();
1215        reader.move_to_next_file().unwrap();
1216        assert_eq!(read_as_vec(&mut reader), vec![0xd, 0xe, 0xf, 0x10]);
1217    }
1218
1219    #[test]
1220    fn test_read_exact() {
1221        let mut temp1 = NamedTempFile::new().unwrap();
1222        let pattern = write_test_pattern(6000, &mut temp1);
1223
1224        let mut reader = SequentialFileReaderBuilder::new()
1225            .read_capacity(512)
1226            .build(2048)
1227            .unwrap();
1228        reader.add_file_to_prefetch(temp1.as_file(), 6000).unwrap();
1229
1230        // Read within a single read_capacity chunk.
1231        let mut buf = [0u8; 100];
1232        reader.read_exact(&mut buf).unwrap();
1233        assert_eq!(buf, pattern[..100]);
1234
1235        // Empty read is a no-op.
1236        reader.read_exact(&mut []).unwrap();
1237        assert_eq!(reader.get_file_offset(), 100);
1238
1239        // Read crossing multiple read_capacity boundaries.
1240        let mut buf = vec![0u8; 2000];
1241        reader.read_exact(&mut buf).unwrap();
1242        assert_eq!(buf, pattern[100..2100]);
1243
1244        // Read remaining data exactly to EOF.
1245        let mut buf = vec![0u8; 3900];
1246        reader.read_exact(&mut buf).unwrap();
1247        assert_eq!(buf, pattern[2100..6000]);
1248
1249        // Reading past EOF returns UnexpectedEof.
1250        let err = reader.read_exact(&mut [0u8; 1]).unwrap_err();
1251        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1252    }
1253}