Don't report `RDHUP` as `HUP` (#939)
Currently, EPOLLRDHUP sets UnixReady::hup(). This is incorrect behavior
because higher-level libraries like tokio (correctly) assume that
UnixReady::hup() is unclearable since it signals that both the read and
write halfs are shutdown. In reality, EPOLLRDHUP only means that the TCP
stream has been half-closed and such a half-closed stream can still be
written to.
This will fix a current issue with tokio, which is that tokio infinitely
attempts to write to a half-closed socket that's returning WouldBlock
when it's write buffer is full, an issue which manifests with excessive
CPU usage. I think this may help some of the issues discussed in
tokio-rs/tokio#449
After this change, EOF will still be propagated correctly, because
read-hangups also trigger read-readiness via EPOLLIN. However, if
handling of EPOLLRDHUP is desired to be retained, I believe it should be
implemented as another readiness kind on UnixReady, perhaps
UnixReady::read_hup().
Possible concern of a breaking change: Since it's not currently possible
for a user of mio to differentiate between EPOLLHUP and EPOLLRDHUP, it
must be that no users of mio currently are. There _may_ be applications
that test the "health" of a socket by checking for UnixRead::hup(),
which would previously trigger on EPOLLRDHUP but will no longer with
this change. This will change such applications from considering a
half-closed connection as closed to considering it open. However, I
still beleive this change is a correction of the semantics of HUP and
the desired behavior such applications was already ambiguous.
This also fixes a similar issue with kqueue.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 603dfad..da052f6 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -1,5 +1,5 @@
-trigger: ["master"]
-pr: ["master"]
+trigger: ["master", "v0.6.x"]
+pr: ["master", "v0.6.x"]
jobs:
# Check formatting
diff --git a/src/sys/unix/epoll.rs b/src/sys/unix/epoll.rs
index 313ae25..03b0ebd 100644
--- a/src/sys/unix/epoll.rs
+++ b/src/sys/unix/epoll.rs
@@ -6,7 +6,7 @@
use std::{cmp, i32};
use libc::{self, c_int};
-use libc::{EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLLONESHOT};
+use libc::{EPOLLERR, EPOLLHUP, EPOLLONESHOT};
use libc::{EPOLLET, EPOLLOUT, EPOLLIN, EPOLLPRI};
use {io, Ready, PollOpt, Token};
@@ -141,11 +141,6 @@
kind |= EPOLLOUT;
}
- if UnixReady::from(interest).is_hup() {
- kind |= EPOLLRDHUP;
- }
-
-
if UnixReady::from(interest).is_priority() {
kind |= EPOLLPRI;
}
@@ -228,7 +223,7 @@
kind = kind | UnixReady::error();
}
- if (epoll & EPOLLRDHUP) != 0 || (epoll & EPOLLHUP) != 0 {
+ if (epoll & EPOLLHUP) != 0 {
kind = kind | UnixReady::hup();
}
diff --git a/src/sys/unix/kqueue.rs b/src/sys/unix/kqueue.rs
index a2ac1ed..371e0cf 100644
--- a/src/sys/unix/kqueue.rs
+++ b/src/sys/unix/kqueue.rs
@@ -291,6 +291,18 @@
event::kind_mut(&mut self.events[idx]).insert(Ready::readable());
} else if e.filter == libc::EVFILT_WRITE as Filter {
event::kind_mut(&mut self.events[idx]).insert(Ready::writable());
+
+ // `EV_EOF` set with `EVFILT_WRITE` indicates the connection has been fully
+ // disconnected (read and write), but only sockets...
+ if e.flags & libc::EV_EOF != 0 {
+ event::kind_mut(&mut self.events[idx]).insert(UnixReady::hup());
+
+ // When the read end of the socket is closed, EV_EOF is set on
+ // flags, and fflags contains the error if there is one.
+ if e.fflags != 0 {
+ event::kind_mut(&mut self.events[idx]).insert(UnixReady::error());
+ }
+ }
}
#[cfg(any(target_os = "dragonfly",
target_os = "freebsd", target_os = "ios", target_os = "macos"))]
@@ -305,16 +317,6 @@
event::kind_mut(&mut self.events[idx]).insert(UnixReady::lio());
}
}
-
- if e.flags & libc::EV_EOF != 0 {
- event::kind_mut(&mut self.events[idx]).insert(UnixReady::hup());
-
- // When the read end of the socket is closed, EV_EOF is set on
- // flags, and fflags contains the error if there is one.
- if e.fflags != 0 {
- event::kind_mut(&mut self.events[idx]).insert(UnixReady::error());
- }
- }
}
ret
diff --git a/src/sys/unix/ready.rs b/src/sys/unix/ready.rs
index d34699a..1780cea 100644
--- a/src/sys/unix/ready.rs
+++ b/src/sys/unix/ready.rs
@@ -198,7 +198,8 @@
/// **Note that only readable and writable readiness is guaranteed to be
/// supported on all platforms**. This means that `hup` readiness
/// should be treated as a hint. For more details, see [readiness] in the
- /// poll documentation.
+ /// poll documentation. It is also unclear if HUP readiness will remain in 0.7. See
+ /// [here][issue-941].
///
/// See [`Poll`] for more documentation on polling.
///
@@ -214,6 +215,7 @@
///
/// [`Poll`]: ../struct.Poll.html
/// [readiness]: ../struct.Poll.html#readiness-operations
+ /// [issue-941]: https://github.com/tokio-rs/mio/issues/941
#[inline]
pub fn hup() -> UnixReady {
UnixReady(ready_from_usize(HUP))
diff --git a/test/mod.rs b/test/mod.rs
index 9619746..2f270c0 100644
--- a/test/mod.rs
+++ b/test/mod.rs
@@ -30,6 +30,7 @@
mod test_smoke;
mod test_tcp;
mod test_tcp_level;
+mod test_tcp_shutdown;
mod test_udp_level;
mod test_udp_socket;
mod test_write_then_drop;
diff --git a/test/test_tcp_shutdown.rs b/test/test_tcp_shutdown.rs
new file mode 100644
index 0000000..60e2750
--- /dev/null
+++ b/test/test_tcp_shutdown.rs
@@ -0,0 +1,73 @@
+use std::net::Shutdown;
+use std::time::Duration;
+
+use mio::{Token, Ready, PollOpt, Poll, Events};
+use mio::net::TcpStream;
+
+macro_rules! wait {
+ ($poll:ident, $ready:ident) => {{
+ use std::time::Instant;
+
+ let now = Instant::now();
+ let mut events = Events::with_capacity(16);
+ let mut found = false;
+
+ while !found {
+ if now.elapsed() > Duration::from_secs(5) {
+ panic!("not ready");
+ }
+
+ $poll.poll(&mut events, Some(Duration::from_secs(1))).unwrap();
+
+ for event in &events {
+ #[cfg(unix)]
+ {
+ use mio::unix::UnixReady;
+ assert!(!UnixReady::from(event.readiness()).is_hup());
+ }
+
+ if event.token() == Token(0) && event.readiness().$ready() {
+ found = true;
+ break;
+ }
+ }
+ }
+ }};
+}
+
+#[test]
+fn test_write_shutdown() {
+ let poll = Poll::new().unwrap();
+
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+
+ let mut ready = Ready::readable() | Ready::writable();
+
+ #[cfg(unix)]
+ {
+ ready |= mio::unix::UnixReady::hup();
+ }
+
+ let client = TcpStream::connect(&addr).unwrap();
+ poll.register(&client,
+ Token(0),
+ ready,
+ PollOpt::edge()).unwrap();
+
+ let (socket, _) = listener.accept().unwrap();
+
+ wait!(poll, is_writable);
+
+ let mut events = Events::with_capacity(16);
+
+ // Polling should not have any events
+ poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
+ assert!(events.iter().next().is_none());
+
+ println!("SHUTTING DOWN");
+ // Now, shutdown the write half of the socket.
+ socket.shutdown(Shutdown::Write).unwrap();
+
+ wait!(poll, is_readable);
+}