-
Notifications
You must be signed in to change notification settings - Fork 0
7978: docs(runtime): document FD table pre-warming workaround #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||||||||||||||||||||||||||||||
| //! Demonstrates pre-warming the Linux file descriptor table to avoid latency | ||||||||||||||||||||||||||||||||||||||
| //! spikes caused by file descriptor table growth in multi-threaded processes. | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! On Linux, the kernel's FD table is grown lazily and protected by RCU | ||||||||||||||||||||||||||||||||||||||
| //! synchronization. In multi-threaded processes, when a syscall like `socket()` | ||||||||||||||||||||||||||||||||||||||
| //! triggers a table resize, the calling thread blocks until all RCU readers | ||||||||||||||||||||||||||||||||||||||
| //! quiesce. This can cause stalls of tens of milliseconds on tokio worker threads, | ||||||||||||||||||||||||||||||||||||||
| //! blocking the entire event loop (not just one task). | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! The workaround is to force the kernel to expand the FD table once per process | ||||||||||||||||||||||||||||||||||||||
| //! (before any runtime starts), by duplicating an FD to a high slot and then | ||||||||||||||||||||||||||||||||||||||
| //! closing it. The kernel never shrinks the FD table during a process's lifetime, | ||||||||||||||||||||||||||||||||||||||
| //! so the capacity persists. | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! This is most relevant for services that open many connections concurrently | ||||||||||||||||||||||||||||||||||||||
| //! (e.g. HTTP servers, connection pools). The pre-warm target should be at least | ||||||||||||||||||||||||||||||||||||||
| //! your expected peak FD count. | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! See: <https://github.com/tokio-rs/tokio/issues/7970> | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! Usage: | ||||||||||||||||||||||||||||||||||||||
| //! | ||||||||||||||||||||||||||||||||||||||
| //! cargo run --example prewarm-fd-table | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #![warn(rust_2018_idioms)] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[cfg(target_os = "linux")] | ||||||||||||||||||||||||||||||||||||||
| fn prewarm_fd_table(target: u32) -> std::io::Result<()> { | ||||||||||||||||||||||||||||||||||||||
| use std::os::unix::io::{FromRawFd, OwnedFd}; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Open /dev/null to get a base FD. | ||||||||||||||||||||||||||||||||||||||
| let dev_null = std::fs::File::open("/dev/null")?; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Use F_DUPFD_CLOEXEC to duplicate to a slot >= target. This forces the | ||||||||||||||||||||||||||||||||||||||
| // kernel to expand the FD table in a single syscall, without clobbering | ||||||||||||||||||||||||||||||||||||||
| // existing FDs. CLOEXEC prevents leaking to child processes. | ||||||||||||||||||||||||||||||||||||||
| let raw = unsafe { | ||||||||||||||||||||||||||||||||||||||
| libc::fcntl( | ||||||||||||||||||||||||||||||||||||||
| std::os::unix::io::AsRawFd::as_raw_fd(&dev_null), | ||||||||||||||||||||||||||||||||||||||
| libc::F_DUPFD_CLOEXEC, | ||||||||||||||||||||||||||||||||||||||
| target, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use std::convert::TryInto;
use std::os::unix::io::{FromRawFd, OwnedFd};
let target = target.try_into().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, e)
})?;
// Open /dev/null to get a base FD.
let dev_null = std::fs::File::open("/dev/null")?;
// Use F_DUPFD_CLOEXEC to duplicate to a slot >= target. This forces the
// kernel to expand the FD table in a single syscall, without clobbering
// existing FDs. CLOEXEC prevents leaking to child processes.
let raw = unsafe {
libc::fcntl(
std::os::unix::io::AsRawFd::as_raw_fd(&dev_null),
libc::F_DUPFD_CLOEXEC,
target,
)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. value:good-to-have; category:bug; feedback: The Gemini AI reviewer is correct! FFI with libc uses c_int, which is i32 in Rust. Using other integer types may work or not. It is better to use the same types as the libc's APIs. |
||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong type passed to variadic FFI
|
||||||||||||||||||||||||||||||||||||||
| if raw < 0 { | ||||||||||||||||||||||||||||||||||||||
| return Err(std::io::Error::last_os_error()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Close both FDs. The table capacity persists. | ||||||||||||||||||||||||||||||||||||||
| let _owned = unsafe { OwnedFd::from_raw_fd(raw) }; | ||||||||||||||||||||||||||||||||||||||
| drop(dev_null); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[cfg(not(target_os = "linux"))] | ||||||||||||||||||||||||||||||||||||||
| fn prewarm_fd_table(_target: u32) -> std::io::Result<()> { | ||||||||||||||||||||||||||||||||||||||
| // FD table pre-warming is Linux-specific. | ||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fn main() { | ||||||||||||||||||||||||||||||||||||||
| const FD_TARGET: u32 = 10_000; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| println!("Pre-warming FD table to {FD_TARGET} entries..."); | ||||||||||||||||||||||||||||||||||||||
| if let Err(e) = prewarm_fd_table(FD_TARGET) { | ||||||||||||||||||||||||||||||||||||||
| eprintln!("Warning: failed to pre-warm FD table: {e}"); | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| println!("FD table pre-warmed successfully."); | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. value:good-to-have; category:bug; feedback: The Augment AI reviewer is correct! The whole example does not make sense for non-Linux runs. It would be better to return early in main() with a message printed on the stdout that the example does nothing on non-Linux. Currently it prints success messages even when it did nothing and this may confuse the users. |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-Linux path reports a false success message. On non-Linux, Suggested adjustment fn main() {
const FD_TARGET: u32 = 10_000;
println!("Pre-warming FD table to {FD_TARGET} entries...");
- if let Err(e) = prewarm_fd_table(FD_TARGET) {
- eprintln!("Warning: failed to pre-warm FD table: {e}");
- } else {
- println!("FD table pre-warmed successfully.");
- }
+ #[cfg(target_os = "linux")]
+ {
+ if let Err(e) = prewarm_fd_table(FD_TARGET) {
+ eprintln!("Warning: failed to pre-warm FD table: {e}");
+ } else {
+ println!("FD table pre-warmed successfully.");
+ }
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ let _ = prewarm_fd_table(FD_TARGET);
+ println!("Skipping FD table pre-warm (Linux-only workaround).");
+ }
// Build the runtime *after* pre-warming.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. value:good-to-have; category:bug; feedback: The CodeRabbit AI reviewer is correct! The whole example does not make sense for non-Linux runs. It would be better to return early in main() with a message printed on the stdout that the example does nothing on non-Linux. Currently it prints success messages even when it did nothing and this may confuse the users. |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Build the runtime *after* pre-warming. | ||||||||||||||||||||||||||||||||||||||
| let rt = tokio::runtime::Builder::new_multi_thread() | ||||||||||||||||||||||||||||||||||||||
| .enable_all() | ||||||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| rt.block_on(async { | ||||||||||||||||||||||||||||||||||||||
| println!("Runtime started. Worker threads will not stall on FD table growth up to {FD_TARGET} FDs."); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because
libc::fcntlis varargs, passingtargetas au32can be ABI-incorrect on some platforms; consider casting to the exact expected C type forF_DUPFD_CLOEXEC(typicallylibc::c_int) to avoid UB in thisunsafecall.Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value:good-to-have; category:bug; feedback: The Augment AI reviewer is correct! FFI with libc uses c_int, which is i32 in Rust. Using other integer types may work or not. It is better to use the same types as the libc's APIs.