This is my second merged contribution to rust-lang, and like the first it lives in the low-level I/O layer.
The problem
When a process runs out of file descriptors, the OS returns EMFILE (the per-process limit) or ENFILE (the system-wide one). Until recently, Rust's standard library decoded both into io::ErrorKind::Uncategorized. If you wanted to react to "too many open files" specifically, you couldn't match on the kind. You had to drop to the raw errno:
if err.raw_os_error() == Some(libc::EMFILE) {
// back off and retry
}
That works, but it leaks a platform detail into code that only wanted a category.
The fix
PR #158326 adds a dedicated variant to io::ErrorKind:
pub enum ErrorKind {
#[unstable(feature = "io_error_inprogress", issue = "130840")]
InProgress,
+ /// The process or the whole system has reached its limit on
+ /// the number of open files or sockets.
+ #[unstable(feature = "io_error_too_many_open_files", issue = "158319")]
+ TooManyOpenFiles,
+
// "Unusual" error kinds ...
Uncategorized,
}
and wires it into the per-platform decode tables, so both errnos map to it on unix and wasi:
libc::EMFILE | libc::ENFILE => TooManyOpenFiles,
with the matching ERROR_TOO_MANY_OPEN_FILES and WSAEMFILE on Windows. So now the category is first class:
match err.kind() {
io::ErrorKind::TooManyOpenFiles => back_off_and_retry(),
_ => return Err(err),
}
The variant is unstable for now (it implements ACP libs-team#818), behind #![feature(io_error_too_many_open_files)].
Why this corner
It is a small change, 13 lines. Reliability work usually is. This is the second PR I have landed in rust-lang's I/O code. The first documented the transient errors TcpListener::accept can hand you, the kind that are easy to treat as fatal by mistake. I am keeping at it on the low-level side, the layer the rest of the ecosystem stands on.
Top comments (2)
Nice one. The ergonomics win is real, but the part worth flagging is that EMFILE and ENFILE now collapse into the same kind. They call for different backoff. EMFILE is per-process, so closing your own descriptors or shrinking a pool usually recovers it. ENFILE is system-wide, so backing off in your process may do nothing because the pressure is global.
So I would still reach for raw_os_error when the retry policy actually differs between the two. Match on TooManyOpenFiles for the generic path, fall through to the errno only when you need to decide whose fault the exhaustion is. Was splitting them into two kinds discussed in the ACP, or was one category the deliberate call?
The category is the right call, and there's a subtlety worth flagging for anyone matching on it: EMFILE and ENFILE both land on TooManyOpenFiles now, but they want different backoff. EMFILE is per-process, so your own fd churn frees the pressure and a short retry usually clears it. ENFILE is the whole box starved, and blind retry there just adds load to a system that is already out. So the variant is great for the match, but the recovery policy still has to drop to raw_os_error to tell 'me' from 'everyone.'
The accept() PR you mention is the one I'd point server authors at. EMFILE on accept is the classic 'listener looks dead under load' bug: treat it as fatal, kill the loop, and the box stops taking connections while it is still perfectly healthy. Categorizing the transient ones so they don't read as fatal is quietly a bigger reliability win than it sounds.