Skip to content

7907: Implement AsyncRead for files with io uring#64

Open
martin-augment wants to merge 5 commits intomasterfrom
pr-7907-2026-02-16-11-06-07
Open

7907: Implement AsyncRead for files with io uring#64
martin-augment wants to merge 5 commits intomasterfrom
pr-7907-2026-02-16-11-06-07

Conversation

@martin-augment
Copy link
Owner

7907: To review by AI

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Walkthrough

The PR introduces changes across Tokio's file I/O and io-uring subsystems. It refactors file read operations to support io-uring optimization through a new private helper function, replaces OwnedFd with reference-counted ArcFd for descriptor management, introduces a generic buffer abstraction via ReadBuffer trait supporting both Vec and Buf types, adds helper methods for buffer preparation and completion, updates cancellation data structures to distinguish buffer types, and includes a comprehensive test suite validating AsyncRead behavior with io-uring on various configurations.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr-7907-2026-02-16-11-06-07

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @martin-augment, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances tokio::fs::File by integrating io_uring for AsyncRead operations. This change aims to improve performance for file I/O on Linux systems by leveraging kernel-level asynchronous operations, while maintaining compatibility with existing spawn_blocking for environments where io_uring is not available or enabled. The implementation includes a new generic buffer trait and robust testing to ensure reliability across different use cases and runtime configurations.

Highlights

  • io_uring Integration for AsyncRead: Implemented AsyncRead for tokio::fs::File using io_uring on Linux, providing a potentially more efficient asynchronous file reading mechanism. This path is conditionally compiled under tokio_unstable and the io-uring feature.
  • Generic ReadBuffer Trait: Introduced a ReadBuffer trait to abstract buffer preparation and completion for io_uring operations, allowing Vec<u8> and Buf to be used interchangeably.
  • Conditional io_uring Fallback: Refactored the File::poll_read implementation to first attempt an io_uring read if the driver is ready, otherwise falling back to the existing spawn_blocking mechanism.
  • io_uring Driver Readiness Check: Added a new is_uring_ready method to the io_uring driver handle, allowing checks for io_uring initialization and Read opcode support without attempting initialization.
  • Comprehensive Test Coverage: Added a new test file (fs_uring_file_read.rs) with extensive tests covering various AsyncRead scenarios, including basic reads, exact reads, read-to-end, large files, custom buffer sizes, seeking, concurrent reads, cancellation, and multi-runtime compatibility.
Changelog
  • spellcheck.dic
    • Updated line count in dictionary
    • Added 'SQE' to the spellcheck dictionary
  • tokio/src/fs/file.rs
    • Added cfg_io_uring block for spawn utility imports
    • Modified AsyncRead implementation to delegate to Inner::poll_read_inner
    • Introduced Inner::poll_read_inner to handle conditional io_uring or spawn_blocking reads
  • tokio/src/fs/mocks.rs
    • Added spawn function within cfg_io_uring for mock testing environments
  • tokio/src/fs/read_uring.rs
    • Imported ArcFd and Arc for shared file descriptor ownership
    • Updated read_to_end_uring and small_probe_read to use ArcFd instead of OwnedFd
    • Modified op_read to accept ArcFd and use Op::read_at
  • tokio/src/io/blocking.rs
    • Added cfg_io_uring block to Buf implementation
    • Implemented prepare_uring_read and complete_uring_read methods for Buf to support io_uring operations
  • tokio/src/io/uring/read.rs
    • Introduced ReadBuffer trait for generic buffer handling in io_uring reads
    • Implemented ReadBuffer for Vec<u8> and Buf types
    • Refactored Read struct to be generic over ReadBuffer
    • Updated Completable implementation to use uring_read_complete for buffer finalization
    • Modified Cancellable trait implementations to differentiate between ReadVec and ReadBuf
    • Replaced Op::read with Op::read_at, making it generic and utilizing uring_read_prepare
  • tokio/src/io/uring/utils.rs
    • Defined ArcFd type alias for Arc<dyn AsRawFd + Send + Sync + 'static>
  • tokio/src/runtime/driver/op.rs
    • Imported Buf for io::blocking
    • Updated CancelData enum to include ReadVec and ReadBuf variants
  • tokio/src/runtime/io/driver/uring.rs
    • Added is_uring_ready method to check io_uring driver readiness and Read opcode support
  • tokio/tests/fs_uring_file_read.rs
    • Added new test file to verify AsyncRead functionality for tokio::fs::File using io_uring
Activity
  • The pull request was created with a request for AI review.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link

claude bot commented Feb 16, 2026

PR Review: Implement AsyncRead for files with io_uring

Summary

This PR implements io_uring support for AsyncRead on tokio::fs::File, providing a performance optimization path that uses io_uring when available while falling back to the existing spawn_blocking approach. The implementation is well-architected with good test coverage.


Code Quality & Architecture ⭐

Strengths

  1. Clean abstraction with ReadBuffer trait (tokio/src/io/uring/read.rs:10-22)

    • The trait abstraction allows both Vec<u8> and Buf to work with io_uring
    • Clear separation of concerns between buffer preparation and completion
    • Well-documented safety contracts
  2. Proper fallback mechanism (tokio/src/fs/file.rs:960-1007)

    • Uses is_uring_ready() to check availability before attempting io_uring path
    • Gracefully falls back to spawn_blocking if io_uring is unavailable
    • Maintains existing behavior for non-io_uring environments
  3. Type-safe file descriptor handling

    • Introduction of ArcFd type alias (tokio/src/io/uring/utils.rs:6) provides clear ownership semantics
    • Changing from OwnedFd to Arc<dyn AsRawFd + Send + Sync> allows shared ownership during async operations
  4. Generic implementation (tokio/src/io/uring/read.rs:91-113)

    • Op::read_at is generic over ReadBuffer, reducing code duplication
    • Well-commented safety invariants

Architectural Decisions

  1. Using u64::MAX for current file position (tokio/src/fs/file.rs:980)

    • This is a clever use of io_uring's offset parameter to maintain cursor semantics
    • Consider adding a const: const URING_USE_CURRENT_OFFSET: u64 = u64::MAX; for clarity
  2. EINTR handling (tokio/src/fs/file.rs:982-987)

    • Correctly retries on ErrorKind::Interrupted
    • This is important for correctness on Linux

Potential Issues 🔍

Critical

None identified - The core implementation appears sound.

Minor Issues

  1. Mock spawn implementation may be fragile (tokio/src/fs/mocks.rs:162-178)

    let _ = tx.send(crate::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(f));
    • Creates a new runtime for each mock spawn call
    • This could be inefficient if tests make many file operations
    • Recommendation: Document why this approach is needed or consider reusing a thread-local runtime
  2. Buffer reserve logic (tokio/src/io/blocking.rs:288)

    self.buf.reserve(max_buf_size);
    let spare = self.buf.spare_capacity_mut();
    let len = std::cmp::min(spare.len(), max_buf_size);
    • After reserve(max_buf_size), spare capacity might still be less than max_buf_size
    • The code handles this correctly with min(), but this could be surprising
    • Consider: assert!(spare.len() >= max_buf_size) if the intention is that reserve guarantees capacity
  3. Debug implementation loses fd information (tokio/src/io/uring/read.rs:54-59)

    • The custom Debug impl doesn't show the fd, making debugging harder
    • Could add: .field("fd", &self.fd.as_raw_fd())

Performance Considerations ⚡

Improvements

  1. Eliminates thread pool overhead

    • io_uring operations don't require thread pool context switches
    • Should significantly reduce latency for I/O-bound workloads
  2. Better batching potential

    • io_uring's SQE/CQE model allows better batching of operations
    • Multiple file reads can be submitted together

Considerations

  1. Memory overhead of Arc

    • Changing from OwnedFd to Arc<dyn AsRawFd> adds allocation overhead
    • This is likely negligible compared to I/O time, but worth noting
    • The Arc is necessary for sharing across the async boundary
  2. Runtime check overhead (tokio/src/fs/file.rs:971-973)

    if let Ok(handle) = crate::runtime::Handle::try_current() {
        let driver_handle = handle.inner.driver().io();
        if driver_handle.is_uring_ready() { ... }
    • This check happens on every read operation
    • Optimization opportunity: Cache the result per-File instance
    • However, the overhead is probably minimal in practice

Security Concerns 🔒

Buffer Safety

  1. Unsafe buffer initialization is correct

    • tokio/src/io/blocking.rs:301-307: The safety contract is well-documented
    • tokio/src/io/uring/read.rs:68-69: Kernel writes are validated
    • The use of spare_capacity_mut() ensures we're not reading uninitialized memory
  2. File descriptor lifetime management

    • The ArcFd ensures the fd stays alive during io_uring operations
    • The cancellation mechanism (CancelData) properly holds resources until completion
    • No TOCTOU issues identified

Observations

  1. No new attack surface - The io_uring path uses the same safety mechanisms as the existing code
  2. Proper error propagation - Errors from io_uring are correctly converted and returned

Test Coverage ✅

Excellent Coverage

The new test file (tokio/tests/fs_uring_file_read.rs) is comprehensive:

  1. Basic functionality: Empty files, small files, large files (3MB)
  2. AsyncRead trait methods: read(), read_exact(), read_to_end(), read_to_string()
  3. Seeking: Forward and backward seeking with reads
  4. Edge cases:
    • Custom buffer sizes (tokio/tests/fs_uring_file_read.rs:137-144)
    • Partial reads (tokio/tests/fs_uring_file_read.rs:147-161)
    • Read after write (tokio/tests/fs_uring_file_read.rs:199-213)
  5. Concurrency: 100 concurrent file operations (tokio/tests/fs_uring_file_read.rs:241-266)
  6. Cancellation: Task abort handling (tokio/tests/fs_uring_file_read.rs:217-240)
  7. Runtime variations: Tests across multiple runtime configurations (tokio/tests/fs_uring_file_read.rs:269-287)
  8. Shutdown safety: Runtime shutdown with pending operations (tokio/tests/fs_uring_file_read.rs:290-316)

Minor Gaps

  1. No test for EINTR handling

    • The retry loop at tokio/src/fs/file.rs:982-987 is untested
    • This is hard to test but worth noting
  2. No test for max_buf_size edge cases

    • What happens with set_max_buf_size(0) or very small values?

API Changes

Breaking Changes

None - All changes are additive and behind feature flags.

New CancelData Variants

  • CancelData::ReadVec and CancelData::ReadBuf replace CancelData::Read
  • This is an internal change and doesn't affect public API

Recommendations

Before Merge

  1. Consider adding const for u64::MAX usage

    const URING_CURRENT_FILE_POS: u64 = u64::MAX;
  2. Add fd to Debug output in tokio/src/io/uring/read.rs:54-59

  3. Document the mock runtime creation in tokio/src/fs/mocks.rs:168-173

Future Improvements

  1. Consider caching the io_uring availability check per-File instance
  2. Add integration tests that verify the fallback path is exercised
  3. Benchmark against the spawn_blocking implementation to quantify improvements

Conclusion

This is a high-quality implementation that follows Tokio's patterns and includes excellent test coverage. The code is safe, performant, and maintains backward compatibility. The abstraction design is clean and extensible.

Recommendation: Approve with minor suggestions

The minor issues noted are suggestions for improvement rather than blocking concerns. The PR is ready to merge as-is, though addressing the recommendations would further improve code quality.

Great work on this implementation! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@tokio/src/io/uring/read.rs`:
- Around line 24-36: The cast of max_len: usize to u32 in impl ReadBuffer for
Vec<u8>::uring_read_prepare is unsafe because it can silently truncate; update
uring_read_prepare to validate the conversion before casting (e.g.,
assert!(max_len <= u32::MAX) or use u32::try_from(max_len).unwrap_or_else(|_|
panic!(...))) so any oversized max_len fails loudly; keep the returned tuple
semantics ((ptr, max_len as u32)) but only perform the as-u32 conversion after
the checked conversion. Ensure the change is applied inside the Vec<u8> impl
ReadBuffer block for the uring_read_prepare method.
- Around line 38-47: The Buf::prepare_uring_read path can silently truncate when
converting lengths to u32; update Buf::prepare_uring_read (called from
ReadBuffer::uring_read_prepare) to explicitly check that the requested max
length fits in a u32 (e.g. use u32::try_from(len).ok() or assert!(len <=
u32::MAX as usize)) and handle the overflow case deterministically (panic with a
clear message or return an error/zero length) instead of performing a blind
cast, so the io-uring length cannot be truncated.

In `@tokio/tests/fs_uring_file_read.rs`:
- Around line 222-243: The test test_file_read_cancel has a race where the
spawned task may finish before handle.abort(), causing unwrap_err() to panic;
fix by ensuring the spawned task is suspended before aborting (e.g., insert a
synchronization point like calling tokio::task::yield_now().await or using a
oneshot/barrier that the task awaits right after opening the file and before
file.read_to_end). Update the task body (the async move closure passed to
tokio::spawn that opens the file and calls file.read_to_end) to await the
synchronization primitive so abort() reliably cancels an in-flight read.
🧹 Nitpick comments (1)
tokio/src/io/blocking.rs (1)

286-293: The len as u32 cast silently truncates if max_buf_size > u32::MAX.

While DEFAULT_MAX_BUF_SIZE is 2 MB and the user-facing set_max_buf_size doesn't document an upper bound, the io-uring Read opcode uses u32 for length, so this aligns with the kernel interface. However, a debug assertion or saturating cast would make the intent explicit and prevent surprises if someone passes an unreasonably large value.

Optional: add a debug assertion
         let len = std::cmp::min(spare.len(), max_buf_size);
         let ptr = spare.as_mut_ptr().cast::<u8>();
+        debug_assert!(len <= u32::MAX as usize, "uring read length exceeds u32::MAX");
         (ptr, len as u32)

Comment on lines +24 to +36
impl ReadBuffer for Vec<u8> {
fn uring_read_prepare(&mut self, max_len: usize) -> (*mut u8, u32) {
assert!(self.spare_capacity_mut().len() >= max_len);
let ptr = self.spare_capacity_mut().as_mut_ptr().cast();
(ptr, max_len as u32)
}

unsafe fn uring_read_complete(&mut self, n: u32) {
// SAFETY: the kernel wrote `n` bytes into spare capacity starting
// at the old self.len(), so self.len() + n bytes are now initialized.
unsafe { self.set_len(self.len() + n as usize) };
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

max_len as u32 truncation is unchecked — could silently produce incorrect reads.

Line 28 casts max_len (a usize) to u32 without any bounds check. While current callers cap this at MAX_READ_SIZE (64 MiB), the ReadBuffer trait is pub(crate) and any future caller passing max_len > u32::MAX would get a silent truncation, leading to a read shorter than the caller expects.

Since this is in a safety-sensitive path (the returned length feeds the io-uring SQE), add an assertion or use try_from:

Proposed fix
 fn uring_read_prepare(&mut self, max_len: usize) -> (*mut u8, u32) {
     assert!(self.spare_capacity_mut().len() >= max_len);
+    let len: u32 = u32::try_from(max_len).expect("max_len must fit in u32 for io-uring SQE");
     let ptr = self.spare_capacity_mut().as_mut_ptr().cast();
-    (ptr, max_len as u32)
+    (ptr, len)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl ReadBuffer for Vec<u8> {
fn uring_read_prepare(&mut self, max_len: usize) -> (*mut u8, u32) {
assert!(self.spare_capacity_mut().len() >= max_len);
let ptr = self.spare_capacity_mut().as_mut_ptr().cast();
(ptr, max_len as u32)
}
unsafe fn uring_read_complete(&mut self, n: u32) {
// SAFETY: the kernel wrote `n` bytes into spare capacity starting
// at the old self.len(), so self.len() + n bytes are now initialized.
unsafe { self.set_len(self.len() + n as usize) };
}
}
impl ReadBuffer for Vec<u8> {
fn uring_read_prepare(&mut self, max_len: usize) -> (*mut u8, u32) {
assert!(self.spare_capacity_mut().len() >= max_len);
let len: u32 = u32::try_from(max_len).expect("max_len must fit in u32 for io-uring SQE");
let ptr = self.spare_capacity_mut().as_mut_ptr().cast();
(ptr, len)
}
unsafe fn uring_read_complete(&mut self, n: u32) {
// SAFETY: the kernel wrote `n` bytes into spare capacity starting
// at the old self.len(), so self.len() + n bytes are now initialized.
unsafe { self.set_len(self.len() + n as usize) };
}
}
🤖 Prompt for AI Agents
In `@tokio/src/io/uring/read.rs` around lines 24 - 36, The cast of max_len: usize
to u32 in impl ReadBuffer for Vec<u8>::uring_read_prepare is unsafe because it
can silently truncate; update uring_read_prepare to validate the conversion
before casting (e.g., assert!(max_len <= u32::MAX) or use
u32::try_from(max_len).unwrap_or_else(|_| panic!(...))) so any oversized max_len
fails loudly; keep the returned tuple semantics ((ptr, max_len as u32)) but only
perform the as-u32 conversion after the checked conversion. Ensure the change is
applied inside the Vec<u8> impl ReadBuffer block for the uring_read_prepare
method.

Comment on lines +38 to +47
impl ReadBuffer for Buf {
fn uring_read_prepare(&mut self, max_len: usize) -> (*mut u8, u32) {
self.prepare_uring_read(max_len)
}

unsafe fn uring_read_complete(&mut self, n: u32) {
// SAFETY: caller guarantees kernel wrote exactly n bytes.
unsafe { self.complete_uring_read(n as usize) };
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Buf::prepare_uring_read and complete_uring_read signatures and implementation
ast-grep --pattern $'impl Buf {
  $$$
  fn prepare_uring_read($$$) {
    $$$
  }
  $$$
}'
echo "---"
rg -n 'prepare_uring_read\|complete_uring_read' --type rust -C5

Repository: martin-augment/tokio

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Check if file exists and read its contents
git ls-files | grep -E 'read\.rs|uring'
echo "---"
# Check the specific file
if [ -f "tokio/src/io/uring/read.rs" ]; then
  echo "File exists. Reading contents:"
  cat -n "tokio/src/io/uring/read.rs"
else
  echo "File not found at expected path"
  # Search for similar files
  find . -name "*.rs" -path "*/io/*" -path "*/uring/*" 2>/dev/null | head -20
fi

Repository: martin-augment/tokio

Length of output: 5254


🏁 Script executed:

#!/bin/bash
# Find the blocking module and Buf struct
find . -path "*/io/blocking*" -name "*.rs" 2>/dev/null
echo "---"
# Search for Buf struct definition and its methods
rg -n 'impl Buf|struct Buf|fn prepare_uring_read|fn complete_uring_read' --type rust -C3 tokio/src/io/

Repository: martin-augment/tokio

Length of output: 4607


🏁 Script executed:

#!/bin/bash
# Read the io_uring specific implementations
sed -n '280,310p' tokio/src/io/blocking.rs

Repository: martin-augment/tokio

Length of output: 1420


Guard against max_buf_size > u32::MAX truncation in Buf::prepare_uring_read.

The prepare_uring_read method at line 286 in tokio/src/io/blocking.rs casts len to u32 without checking for overflow. If max_buf_size or spare.len() exceeds u32::MAX, the cast will silently truncate, returning an incorrect length to the io-uring operation. Add an explicit overflow check or assertion to prevent this.

🤖 Prompt for AI Agents
In `@tokio/src/io/uring/read.rs` around lines 38 - 47, The Buf::prepare_uring_read
path can silently truncate when converting lengths to u32; update
Buf::prepare_uring_read (called from ReadBuffer::uring_read_prepare) to
explicitly check that the requested max length fits in a u32 (e.g. use
u32::try_from(len).ok() or assert!(len <= u32::MAX as usize)) and handle the
overflow case deterministically (panic with a clear message or return an
error/zero length) instead of performing a blind cast, so the io-uring length
cannot be truncated.

Comment on lines +222 to +243
async fn test_file_read_cancel() {
let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let (_tmp, path) = create_temp_file(&data);

let path2 = path.clone();
let handle = tokio::spawn(async move {
let mut file = File::open(&path2).await.unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).await.unwrap();
buf
});

handle.abort();
let res = handle.await;
assert!(res.unwrap_err().is_cancelled());

// Verify runtime still works
let mut file = File::open(&path).await.unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).await.unwrap();
assert_eq!(buf, data);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential test flakiness in cancel test.

There's a race between the spawned task completing and the abort() call. If the task finishes before abort(), handle.await returns Ok(buf) and unwrap_err() panics. While unlikely in practice (the task must open the file and read 10KB before the immediate abort()), this could be flaky on fast systems or under light load.

Consider adding a synchronization point (e.g., a barrier or tokio::task::yield_now() inside the task before the read) to ensure the task is in-flight when abort() is called.

🤖 Prompt for AI Agents
In `@tokio/tests/fs_uring_file_read.rs` around lines 222 - 243, The test
test_file_read_cancel has a race where the spawned task may finish before
handle.abort(), causing unwrap_err() to panic; fix by ensuring the spawned task
is suspended before aborting (e.g., insert a synchronization point like calling
tokio::task::yield_now().await or using a oneshot/barrier that the task awaits
right after opening the file and before file.read_to_end). Update the task body
(the async move closure passed to tokio::spawn that opens the file and calls
file.read_to_end) to await the synchronization primitive so abort() reliably
cancels an in-flight read.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces io_uring support for AsyncRead on tokio::fs::File, which is a great performance enhancement. The implementation is well-structured, introducing a generic ReadBuffer trait and using ArcFd to handle file descriptors. The changes are gated behind the appropriate feature flags and are accompanied by a comprehensive set of new tests, which is excellent. My main feedback is to address a piece of duplicated logic for handling interrupted syscalls, which could be refactored into a shared helper function to improve maintainability.

Comment on lines +978 to +991
loop {
let (res, r_fd, r_buf) =
// u64::MAX to use and advance the file position
Op::read_at(fd, buf, max_buf_size, u64::MAX).await;
match res {
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
buf = r_buf;
fd = r_fd;
continue;
}
Err(e) => break (Operation::Read(Err(e)), r_buf),
Ok(n) => break (Operation::Read(Ok(n as usize)), r_buf),
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop to handle io::ErrorKind::Interrupted is duplicated. A very similar loop exists in tokio/src/fs/read_uring.rs in the op_read function (lines 122-137).

To improve maintainability and avoid future inconsistencies, this logic should be extracted into a shared helper function. For example, a function like read_at_uninterruptible could be added to tokio/src/io/uring/read.rs or another shared module.

Example implementation:

async fn read_at_uninterruptible<B: ReadBuffer + ...>(...) -> (io::Result<u32>, ArcFd, B) {
    loop {
        let (res, r_fd, r_buf) = Op::read_at(fd, buf, max_len, offset).await;
        if let Err(e) = &res {
            if e.kind() == io::ErrorKind::Interrupted {
                fd = r_fd;
                buf = r_buf;
                continue;
            }
        }
        return (res, r_fd, r_buf);
    }
}

Then, both this file and read_uring.rs could call this helper, simplifying the code in both places.

@augmentcode
Copy link

augmentcode bot commented Feb 16, 2026

🤖 Augment PR Summary

Summary: This PR adds an io-uring-backed fast path for tokio::fs::File reads, enabling AsyncRead to use io-uring on Linux when the unstable io-uring runtime is available.

Changes:

  • Routes File::poll_read through a new Inner::poll_read_inner helper that prefers io-uring reads when the driver is ready, with a spawn_blocking fallback.
  • Introduces ArcFd (an Arc<dyn AsRawFd + Send + Sync>) to keep file descriptors alive across in-flight io-uring ops.
  • Generalizes the io-uring read operation via a ReadBuffer trait so reads can target either Vec<u8> or Tokio’s internal Buf.
  • Adds Buf helpers to prepare/complete reads into spare capacity for io-uring.
  • Updates cancellation bookkeeping to distinguish Read<Vec<u8>> vs Read<Buf>.
  • Adds Handle::is_uring_ready to check readiness without attempting initialization.
  • Refactors fs::read_uring to use ArcFd and Op::read_at.
  • Adds a new integration test suite covering common file read patterns, cancellation, concurrency, and runtime shutdown scenarios.
  • Updates the spellcheck dictionary to include SQE.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

let spare = self.buf.spare_capacity_mut();
let len = std::cmp::min(spare.len(), max_buf_size);
let ptr = spare.as_mut_ptr().cast::<u8>();
(ptr, len as u32)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ptr, len as u32) relies on len <= u32::MAX; if max_buf_size exceeds that, the truncating cast can make the io-uring read length unexpectedly small (even 0), potentially causing a premature EOF to be reported to AsyncRead callers. Is it worth adding a guard to prevent silent truncation here?

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

for (i, (_tmp, path)) in files.iter().enumerate() {
let path = path.clone();
let expected: Vec<u8> = (0..1024).map(|j| ((i as u16 + j) % 256) as u8).collect();
tracker.spawn(async move {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JoinHandle returned by tracker.spawn(...) is dropped, so panics inside the task (including the assert_eq!) may not reliably fail the test. It may be worth ensuring task failures are observed so this test actually validates concurrent reads.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments