-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
io: add Ready::ERROR
and report error readiness
#5781
Conversation
67f9465
to
5cd4706
Compare
Ready::ERROR
and report error readiness
bcbb35e
to
2f6b71f
Compare
Is this blocked on anything? anything I can do to help it move along? |
Over the past month, I've been busy with other things, but I should be able to look at it soon now that we have entered July. |
tokio/src/io/ready.rs
Outdated
// error readiness is reported regardless of interest | ||
ready |= Ready::ERROR; | ||
|
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.
Hmm. Does this mean that the fn ready
methods on socket types always return Error
unconditionally even if there are no Ready
events?
We should probably have a test that Ready
is not set if there have been no error events.
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.
the phrasing is misleading I think, I'll improve it. What is happening here is that with this change, an event with the error bit set (i.e. with the error readiness) matches any interest.
so no matter what interest a user configures, an event with error readiness is not discarded. That means the corresponding waker is woken up, and then can deal with the event as they see fit.
We already have some tests that perform an equality check on Ready
values, e.g.
#[tokio::test]
async fn clear_ready_matching_clears_ready() {
use tokio::io::{Interest, Ready};
let (a, mut b) = socketpair();
let afd_a = AsyncFd::new(a).unwrap();
b.write_all(b"0").unwrap();
let mut guard = afd_a
.ready(Interest::READABLE | Interest::WRITABLE)
.await
.unwrap();
// the readiness is just readable and writable, not error
assert_eq!(guard.ready(), Ready::READABLE | Ready::WRITABLE);
guard.clear_ready_matching(Ready::READABLE);
assert_eq!(guard.ready(), Ready::WRITABLE);
guard.clear_ready_matching(Ready::WRITABLE);
assert_eq!(guard.ready(), Ready::EMPTY);
}
This test passes, so the error readiness is not set in this case.
The implementation looks fine, but my main concern is how the error readiness is always exposed. Are we sure we want to do that? What about portability? Linux with epoll is one thing, but what about kqueue? What about Windows? |
I am out of town and reviewing on a mobile device so I may be reading it wrong. I think the biggest question is whether we should always unconditionally report error readiness without the user asking for it on all platforms. I know that this is what epoll does (error interest is always implicit) but I don’t know if we can maintain that behavior across all platforms (windows, future wasi based platforms, etc…) The easiest option is to conditionally define Interest::Error on platforms where we can support it and require the user to specify it when awaiting for readiness. |
This is a fair concern. I don't think always reporting the error readiness has real downsides, but may be missing something. I like the idea of Tokio wraps the I've written more about it in the related issue #5716 (comment) |
Tokio already supports cases where you are waiting for a different set than registered with mio. For example, if two threads are waiting for read and write readiness respectively, then we register with both on mio, but the read waiter is only notified if mio gives us a read readiness.
If you get the error readiness no matter what, then we can just remove it from the readiness set given to mio. |
ah, interesting. So if I use am I understanding the idea correctly? |
You bring up a good point. How does clearing the error readiness work? How does epoll handle it? |
I've added pub struct Interest {
mio: Option<mio::Interest>,
has_error_interest: bool,
} In practice this will take 2 bytes in memory, up from 1 before. This representation makes some existing functions a bit longer, especially because some are Then, mio has this note on the
Based on https://docs.rs/mio/latest/mio/struct.Poll.html#implementation-notes only windows would never use the error field (but the function is still available on that platform). Always providing the error interest and related functions can be convenient to cut down on conditional compilation. On the other hand it can be confusing. I went with just doing what I've verified that this code works as I'd expect with the current code of this PR let mut guard1 = async_fd_socket.ready(Interest::ERROR).await.unwrap();
guard1.clear_ready_matching(Ready::ERROR);
// blocks!
let mut guard2 = async_fd_socket.ready(Interest::ERROR).await.unwrap(); but this is all tokio of course. So I ran another test where I send some bytes to the let mut guard1 = async_fd_socket.ready(Interest::ERROR).await.unwrap();
guard1.clear_ready_matching(Ready::ERROR);
// .. send bytes to `async_fd_socket`
// blocks!
let mut guard2 = async_fd_socket.ready(Interest::ERROR).await.unwrap(); and that still blocks. So the error readiness does not persist between different returns of Is that what you meant with your comment about clearing error readiness @Darksonn? |
That answers how error readiness is cleared for |
At the moment That solution could be to not allow a custom interest, or some equivalent of |
Does that mean that even if an error event occurs, the current implementation will not surface it via the TcpStream api? |
As far as I can see, yes. Because it is not possible to register a tokio if interest.is_error() {
ready |= Ready::ERROR;
} in |
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.
Thanks, it looks mostly good to me. I left a comment on the Interest
struct.
I think, for me, the primary question is, from a portability POV, what are the guarantees Tokio provides when one sets Error
interest? The fact that error is overloaded makes me uncomfortable with providing this as a portable flag.
e.g. it seems to me that error interest should mean that if one receives error readiness, one can just drop the socket w/o performing further operations on it because the socket is in an error state. This doesn't hold for the case you want to use it for.
One option is, we include it for all platforms and document it as "this passes error interest to the underlying OS selector. Behavior is platform specific, read your platform's documentation"
tokio/src/io/interest.rs
Outdated
@@ -11,43 +11,59 @@ use std::ops; | |||
/// I/O resource readiness states. | |||
#[cfg_attr(docsrs, doc(cfg(feature = "net")))] | |||
#[derive(Clone, Copy, Eq, PartialEq)] | |||
pub struct Interest(mio::Interest); | |||
pub struct Interest { |
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.
Could you change this to a usize
and define the bits yourself, similar to how Ready
does it: https://github.com/tokio-rs/tokio/blob/master/tokio/src/io/ready.rs.
I would prefer not growing the struct size and avoid branches in the is_*
checks.
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.
is there a particular reason to choose for usize
? Maybe because the interest value doesn't live long and is likely in a register, usize
is no worse than u8
(which would currently fit, with 2 bits to spare)?
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.
usize
doesn't matter really, it is just what mio::Interest
uses.
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.
mio uses a NonZeroU8
https://github.com/tokio-rs/mio/blob/master/src/interest.rs#L17
#[derive(Copy, PartialEq, Eq, Clone, PartialOrd, Ord)]
pub struct Interest(NonZeroU8);
b1f4652
to
e4e4a03
Compare
allright, I made the changes and CI is happy again. For If eventually the tokio |
That's right.
Given that, I'd be totally ok with a Lifting the conditional compilation restrictions can always happen at a later time, e.g. if a real usecase for error readiness with kqueue comes up. does that sound like a good way forward @carllerche ? |
so, how do we make progress here? |
tokio/tests/io_async_fd.rs
Outdated
// Set the destination address. This address is invalid in this context. the OS will notice | ||
// that nobody is listening on port 1234. Normally this is ignored (UDP is "fire and forget"), | ||
// but because IP_RECVERR is enabled, the error will actually be reported to the sending socket | ||
let mut dest_addr = | ||
unsafe { std::mem::MaybeUninit::<libc::sockaddr_in>::zeroed().assume_init() }; | ||
dest_addr.sin_family = libc::AF_INET as _; | ||
dest_addr.sin_port = 1234u16.to_be(); // Destination port |
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.
So this test will fail if some other test decides to use port 1234 for something?
Perhaps we should use a port number less than 1024 to avoid the case where tests using port 0 to pick a port will pick the port used by this test?
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.
yes, testing this is kind of tricky. I've changed the port to 512
and added an explanation.
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.
Thanks!
tokio/tests/io_async_fd.rs
Outdated
let fd = AsyncFd::new(socket).unwrap(); | ||
|
||
let buf = b"hello there"; | ||
fd.get_ref().send(buf).unwrap(); | ||
|
||
// the send timestamp should now be in the error queue | ||
let guard = fd.ready(Interest::ERROR).await.unwrap(); | ||
assert_eq!(guard.ready(), Ready::ERROR); | ||
} |
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.
Could we assert that the error interest is not set before we send the buffer? Currently all of our tests would pass if ERROR is always set.
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.
yes, I added one of those select!
with a timer and panic branch that is used in a bunch of the other tests.
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.
Looks good to me. Thanks for sticking w/ it!
Motivation
fixes #5716
The motivation is to await messages arriving in a UdpSocket's error queue. The error queue is a mechanism to asynchronously process errors on a socket. By separating them from the normal queue, error information does not interfere with standard IO, but at the same time better error messages can be given than just a single integer as the result of send/receive.
This potentially has many use cases. My particular one is awaiting when a send timestamp is available.
Solution
The solution is to provide
Ready::ERROR
. This is already supported inmio
, but so far was not exposed in tokio. In combination withAsyncFd::ready
with some arbitrary interest, this can be used to await error readiness.One potentially controversial change is that error readiness is reported regardless of interest. That is in practice how error readiness works on operating systems: errors are always reported, no matter what the configured interest is.
Testing
Send timestamping, my primary use case, only works on linux. I've provided a test for it, but it is only compiled/executed on linux.
I've also included a test that sends a UDP packet to a local socket where nobody is listening. Normally this send operation would just succeed, but by setting the
IP_RECVERR
socket option, the sending socket is notified via the error queue that there is nobody listening on the other side. Turns out this test also only works on linux.I'm not sure what to do about other operating systems. because we're just re-exposing mio functionality here maybe it's fine to just have the linux tests on CI?