Skip to content

7846: fs: implement tokio::fs::hard_link via io_uring#50

Open
martin-augment wants to merge 1 commit intomasterfrom
pr-7846-2026-01-09-06-45-50
Open

7846: fs: implement tokio::fs::hard_link via io_uring#50
martin-augment wants to merge 1 commit intomasterfrom
pr-7846-2026-01-09-06-45-50

Conversation

@martin-augment
Copy link
Copy Markdown
Owner

7846: To review by AI

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 9, 2026

Walkthrough

This change introduces io-uring support for the hard_link filesystem operation in Tokio. A new link module is added under the io-uring subsystem with a Link struct that encapsulates the operation and implements required traits for completion and cancellation. The hard_link function in the public API is enhanced with a conditional fast-path that uses the io-uring operation when available on Linux, otherwise falling back to the existing asyncify mechanism. The cancellation data enum is extended to handle link operation cancellation. A comprehensive test suite validates the new functionality across various runtime configurations and edge cases.

✨ Finishing touches
  • 📝 Generate docstrings

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.

@claude
Copy link
Copy Markdown

claude Bot commented Jan 9, 2026

Pull Request Review: fs: implement tokio::fs::hard_link via io_uring

Summary

This PR implements tokio::fs::hard_link using io_uring's LinkAt operation, following the established pattern for io_uring filesystem operations in tokio. Overall, the implementation is solid and follows existing conventions well.


✅ Code Quality & Best Practices

Strengths:

  • Consistent with existing patterns: The implementation follows the same pattern used in open.rs, read.rs, and write.rs for io_uring operations
  • Proper feature gating: Correctly uses the conditional compilation flags matching other uring operations
  • Good fallback mechanism: Properly falls back to thread pool execution when io_uring is unavailable or the opcode isn't supported
  • Documentation: The #[allow(dead_code)] attributes with explanatory comments are helpful

Code structure (tokio/src/io/uring/link.rs):

  • Clean separation of concerns with Completable and Cancellable trait implementations
  • Follows the established pattern for CString lifetime management

🐛 Potential Issues

1. Return Type Consistency (tokio/src/io/uring/link.rs:26)

The complete method returns cqe.result.map(|_| ()), which discards the result value. While this is correct for hard_link (which doesn't need to return data), it's worth noting that the kernel returns the result code which could be useful for debugging.

Current:

fn complete(self, cqe: CqeResult) -> Self::Output {
    cqe.result.map(|_| ())
}

This is actually fine - consistent with how other void operations would work.

2. Test Implementation Detail (tokio/tests/fs_uring_link.rs:61-70)

In shutdown_runtime_while_performing_io_uring_ops, operations are spawned but the futures aren't stored anywhere after the first poll, leading to immediate cancellation. This appears intentional for testing cancellation behavior during shutdown, but the comment could be clearer.

Suggestion: Add a clarifying comment:

// Spawn operations and poll once to register them with io_uring,
// then drop the futures to test cancellation during shutdown

⚡ Performance Considerations

Positive:

  • ✅ Uses io_uring for truly async I/O, avoiding thread pool overhead when available
  • ✅ Proper use of AT_FDCWD for path-based operations
  • ✅ No unnecessary allocations beyond required CString conversions

Note:

  • The check_and_init call on every operation adds a small overhead, but this is consistent with the existing codebase pattern and necessary for probe checking

🔒 Security Concerns

Overall: Good security posture

Proper lifetime management: CString fields are held for the operation duration with #[allow(dead_code)] annotations
Safe POSIX interface: Uses LinkAt with AT_FDCWD, which is the safe way to handle paths
No path traversal issues: Paths are properly converted to CStrings, and the kernel handles validation
SAFETY comment: The unsafe block in Op::link() has an appropriate SAFETY comment

Minor consideration:

  • Hard links can only be created within the same filesystem. The implementation correctly propagates kernel errors, so attempts to link across filesystems will fail with appropriate error codes (EXDEV).

🧪 Test Coverage

Excellent test coverage:

Runtime shutdown during operations (shutdown_runtime_while_performing_io_uring_ops):

  • Tests multiple runtime configurations (current_thread, multi_thread with varying worker counts)
  • Validates graceful cancellation behavior

High concurrency test (hard_link_many_files):

  • Creates 10,000 hard links concurrently
  • Validates correctness by checking inode numbers match
  • Tests across all runtime configurations

Cancellation test (cancel_op_future):

  • Verifies that futures can be properly cancelled
  • Checks that the cancellation is detected correctly

Test quality observations:

  1. The tests follow the pattern from fs_uring_read.rs closely ✅
  2. Good use of TaskTracker and JoinSet for managing concurrent operations ✅
  3. Proper cleanup with TempDir
  4. Inode verification is a solid correctness check ✅

📝 Minor Suggestions

  1. tokio/tests/fs_uring_link.rs:62: The comment says "spawning a bunch of uring operations" but could be more specific about the test's purpose (testing cancellation during shutdown).

  2. Consider adding a test case for error conditions:

    • Attempting to link a non-existent file
    • Attempting to link across filesystems (should return EXDEV)
    • Permission denied scenarios

    However, this is not critical as the error handling path is the same as the thread pool implementation.


✨ Summary

This is a high-quality implementation that:

  • ✅ Follows tokio's established patterns for io_uring operations
  • ✅ Has comprehensive test coverage
  • ✅ Handles edge cases properly (fallback, cancellation, shutdown)
  • ✅ Maintains security invariants
  • ✅ Provides performance benefits through io_uring

Recommendation: Approve with minor suggestions

The implementation is production-ready. The suggestions above are minor enhancements rather than blocking issues.


Great work! 🎉

Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (2)
tokio/tests/fs_uring_link.rs (2)

40-90: shutdown_runtime_while_performing_io_uring_ops likely doesn’t exercise current_thread runtime, and may be too “stress-y” for CI.

Line 40–90: rt.spawn(...) without a block_on/enter driver means the current_thread runtime (Line 42) may never actually run the spawned task; shutdown then “passes” without covering io_uring ops. Also, multi_rt(256) + the unbounded for i in 0..usize::MAX spawn loop can be flaky/heavy.

Possible tightening (example approach)
  • Exclude current_rt() from this test (or restructure to actively drive it).
  • Bound the work by time or a fixed task count (e.g., 5k) and/or lower max worker threads for CI stability.

132-160: cancel_op_future currently proves task abortion, not necessarily io_uring op cancellation.

Line 132–160: since you don’t assert the first poll is Pending (and hard_link could complete immediately or fall back to the threadpool), the test can pass without ever having an in-flight uring op to cancel. If the intent is “uring cancellation path works”, it likely needs a stronger signal that the operation was submitted and is still pending.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe52b2 and f67dbbb.

📒 Files selected for processing (5)
  • tokio/src/fs/hard_link.rs
  • tokio/src/io/uring/link.rs
  • tokio/src/io/uring/mod.rs
  • tokio/src/runtime/driver/op.rs
  • tokio/tests/fs_uring_link.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-30T14:46:01.443Z
Learnt from: martin-augment
Repo: martin-augment/tokio PR: 10
File: tokio-util/src/io/mod.rs:17-17
Timestamp: 2025-10-30T14:46:01.443Z
Learning: In tokio-util, the entire `io` module at `tokio-util/src/lib.rs` is already gated with `cfg_io!` macro, which applies `#[cfg(feature = "io")]` to all contents. Submodules declared within `tokio-util/src/io/mod.rs` inherit this feature gate and don't need additional `cfg_io!` wrapping unless they have specific requirements. The `cfg_io_util!` macro in the same file is for items that specifically require the `io-util` feature, not the general `io` feature.

Applied to files:

  • tokio/src/io/uring/mod.rs
🧬 Code graph analysis (5)
tokio/src/io/uring/mod.rs (1)
tokio/src/io/uring/link.rs (1)
  • link (43-57)
tokio/src/runtime/driver/op.rs (1)
tokio/src/io/uring/link.rs (1)
  • link (43-57)
tokio/src/io/uring/link.rs (2)
tokio/src/io/uring/utils.rs (1)
  • cstr (4-6)
tokio/src/runtime/driver/op.rs (4)
  • complete (117-117)
  • complete_with_error (123-123)
  • cancel (128-128)
  • new (66-73)
tokio/src/fs/hard_link.rs (2)
tokio/src/runtime/handle.rs (1)
  • current (144-148)
tokio/src/io/uring/link.rs (1)
  • link (43-57)
tokio/tests/fs_uring_link.rs (3)
tokio/src/runtime/driver/op.rs (2)
  • new (66-73)
  • poll (136-208)
tokio/src/io/uring/link.rs (1)
  • link (43-57)
tokio/src/fs/hard_link.rs (1)
  • hard_link (39-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
tokio/tests/fs_uring_link.rs (2)

92-130: hard_link_many_files is solid; consider limiting concurrency if CI shows sporadic failures.

Line 92–130: this is a good correctness check (dev+ino equality). If CI gets noisy, consider batching the 10k metadata tasks to avoid saturating the scheduler/file system.


162-172: Helper looks fine.

Line 162–172: create_tmp_dir is straightforward and creates the target file in a temp directory as expected.

tokio/src/io/uring/mod.rs (1)

1-1: The new link module is properly cfg-gated.

The entire uring module is wrapped with cfg_io_uring! at the parent level in tokio/src/io/mod.rs, so all submodules—including link—automatically inherit this feature gate. The sibling modules (open, read, utils, write) follow the same pattern without additional cfg wrapping, confirming that this approach is consistent throughout the codebase.

tokio/src/runtime/driver/op.rs (1)

1-4: The imports in op.rs are properly gated and safe. Both op.rs and the uring module (including link.rs) are conditionally compiled with cfg_io_uring!, which expands to #[cfg(all(tokio_unstable, feature = "io-uring", feature = "rt", feature = "fs", target_os = "linux"))]. Since both the importer and the imported modules share identical cfg guards, there is no risk of a build break from missing definitions.

Likely an incorrect or invalid review comment.

tokio/src/io/uring/link.rs (1)

11-58: Implementation is correct; cfg-gating and LinkAt opcode API verified.

The Link struct properly stores CStrings to keep kernel pointers valid throughout the operation lifetime, and LinkAt::new() parameter order (olddirfd, oldpath, newdirfd, newpath) matches both the code usage and the linkat(2) system call. The uring module is already feature-gated with cfg_io_uring! in tokio/src/io/mod.rs, so link.rs inherits this protection without needing additional cfg directives. The Completable and Cancellable implementations correctly handle operation completion and cancellation.

tokio/src/fs/hard_link.rs (1)

39-56: Implementation is correct. io_uring::opcode::LinkAt::CODE exists in io-uring 0.7.11 and properly corresponds to linkat(2). The check_and_init state machine handles supported/unsupported scenarios correctly and is idempotent (cheap on subsequent calls).

The fast-path wiring is sound. The conditional io-uring block correctly:

  • Uses LinkAt::CODE constant from io-uring 0.7.11
  • Calls check_and_init to verify io_uring is available and the linkat opcode is supported
  • Returns Ok(true) only when both are available; Ok(false) gracefully when either is unsupported (ENOSYS for kernel too old, EPERM for seccomp)
  • Falls back cleanly to asyncify for blocking I/O when io_uring is unavailable

The state machine is idempotent: first call performs setup; subsequent calls are fast checks. This pattern is consistently used across filesystem operations (read, write, open).

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Jan 9, 2026

🤖 Augment PR Summary

Summary: Implements an io_uring-backed fast path for tokio::fs::hard_link on Linux when the unstable io_uring runtime is enabled.

Changes:

  • Update tokio::fs::hard_link to prefer io_uring LinkAt when supported, otherwise fall back to the existing asyncify/std::fs::hard_link path.
  • Add a new io_uring operation wrapper (tokio::io::uring::link) implementing Completable/Cancellable and Op::link.
  • Expose the new module from tokio::io::uring and wire cancellation storage via CancelData::Link.
  • Add io_uring-focused tests covering runtime shutdown while ops are in-flight, high-volume link creation, and task cancellation.

Technical Notes: Uses Handle::current() + driver_handle.check_and_init(LinkAt::CODE) to probe opcode support and keeps path CStrings alive for the kernel for the op’s full lifetime.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@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. 1 suggestions posted.

Fix All in Augment

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


rt.spawn(async move {
// spawning a bunch of uring operations.
for i in 0..usize::MAX {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This loop can create an unbounded number of distinct hard-link directory entries before shutdown (bounded only by timing), which risks inode/disk exhaustion or CI flakiness. Consider bounding the number of iterations/links (similar to other uring shutdown stress tests) so the test remains robust across slower machines.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:incorrect-but-reasonable; category:bug; feedback:The Augment AI reviewer is partially correct! Indeed using usize::MAX would lead to a huge number of spawned tasks but the test also spawns a new OS thread that shuts down the Tokio runtime after 300ms. The test actually tests that the shutdown works even when there are active tasks. So, the number of tasks might become big but it won't really loon until usize::MAX.

poll_fn(|cx| {
assert_pending!(fut.as_mut().poll(cx));
Poll::<()>::Pending
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test assertion fails if io_uring operation completes quickly

Medium Severity

The poll_fn closure always returns Poll::Pending, but when the io_uring hard_link operation completes, the task gets woken and poll_fn is polled again. On subsequent polls, fut.as_mut().poll(cx) returns Poll::Ready (since the operation completed), causing assert_pending! to panic. This can cause intermittent test failures when filesystem operations complete quickly between polls.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback:The Augment AI reviewer is correct! The test would not be very stable because it assumes that the io_uring operation is still being processed by the Linux kernel and the first poll will be always Pending. But it might be that the issued io_uring operation finishes before the first assertion and in this case the result will be Poll::Ready and it will fail. Prevents introducing a flacky test that will fail randomly.

@martin-augment
Copy link
Copy Markdown
Owner Author

martin-augment commented Jan 12, 2026

40-90: shutdown_runtime_while_performing_io_uring_ops likely doesn’t exercise current_thread runtime, and may be too “stress-y” for CI.

Line 40–90: rt.spawn(...) without a block_on/enter driver means the current_thread runtime (Line 42) may never actually run the spawned task; shutdown then “passes” without covering io_uring ops.

value:useful; category:bug; feedback:The CodeRabbit AI reviewer is correct! The CurrentThreadRuntime needs to wrap the tested logic in block_on() call to actually execute async tasks. The same change would work for the multi thread runtime too. Prevents introducing new tests which actually do not execute and do not test anything.

@martin-augment
Copy link
Copy Markdown
Owner Author

132-160: cancel_op_future currently proves task abortion, not necessarily io_uring op cancellation.
Line 132–160: since you don’t assert the first poll is Pending (and hard_link could complete immediately or fall back to the threadpool), the test can pass without ever having an in-flight uring op to cancel. If the intent is “uring cancellation path works”, it likely needs a stronger signal that the operation was submitted and is still pending.

value:useful; category:bug; feedback:The CodeRabbit AI reviewer is correct! There is no easy way to test that an io_uring operation executing in the Linux kernel can be cancelled due to timing issues. The test verifies that the Tokio task can be cancelled and its result be ignored, i.e. only the user-space logic could be cancelled at any time.

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