-
-
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 AsyncFd #2903
io: Add AsyncFd #2903
Conversation
macos tests are failing because |
03399d8
to
dd728fe
Compare
Force pushing to rebase on master. |
Discussed the API a bit with @carllerche (particularly around whether it should own the FD). I feel that Also, @seanmonstar , do you have any opinions or concerns around this API? |
@bdonlan maybe I'm lacking some context, but could you elaborate on the non-owned use cases? I'm still worried that having a non-owned-by-default API will lead to various footguns with juggling drop ordering. Consider a struct which holds some owned fd type and an |
@ipetkov It's possible, for example, for the file descriptor to be owned by some native library, where we're not allowed to close it. There are workarounds of course if we were to implement closing of FDs (eg. duping the file descriptor, or implementing Personally, I'd be more of a fan of the owned API - @carllerche, what do you think about the concerns above? |
@bdonlan not necessarily suggesting we use generics but Alternatively, if we really want to avoid generics or traits, we could have some kind of |
Generics feels like it might be the right approach, but one issue is that many IO objects will need mutable access. Consider instantiating an |
@bdonlan what if |
I don't necessarily want to assume that the fd is meant to be used via |
I think as long as As for preventing duplicate ownership- I think ensuring that an Alternatively, we could look into ensuring that FDs are refcounted, and the only true owner is the refcounter; anything using the FD would "subscribe" to it and increase the refcount until it's ready to release it. The above two options are the only ones I see being sensible under the single-ownership-principle Rust enforces. |
After reading the comments, if this is a type that takes ownership of the FD, I like the idea of it being That said, it would add an API wrinkle. How would you get |
This is currently pending on tokio-rs/tokio#2903 in order to support registering a fd as a tokio Source.
Why not expose the The suggested work-around of allocating a |
I also think that I'm really not a fan of using |
After some thought, I don't think it's necessary to get What I wonder here is whether we should offer a new trait to extract the fd that can see through common mutexes like this, something like: trait FdHolder {
type Output: Future<Output=RawFd>;
fn get_fd(&self) -> Output;
}
impl<T: AsRawFd> FdHolder for T { ... }
impl<T: AsRawFd> FdHolder for RefCell<T> { ... }
impl<T: AsRawFd> FdHolder for std::mutex::Mutex<T> { ... }
impl<T: FdHolder> FdHolder for tokio::sync::Mutex<T> { ... } or if it would be better to just accept a conversion function for types not implementing AsRawFd: impl<T> AsyncFd {
async fn new(obj: T) where T: AsRawFd { ... }
async fn new_with_fd_extracter(obj: T, fd_extract: impl FnOnce(&mut T)->RawFd) { ... }
} or if we should just let the user pass a fd directly: impl<T> AsyncFd {
async fn new(obj: T) where T: AsRawFd { ... }
async fn new_with_fd(obj: T, fd: RawFd) { ... }
} |
@bdonlan Ok, that sounds good to me. It is also possible to add mut support in the future by adding a I'm also fine adding So, in conclusion, we are going to change |
I'm working on updating this with the feedback here - before I go too far, what are people's thoughts on the feature flag for this - define a new one or use |
Previously, there was a race window in which an IO driver shutting down could fail to notify ScheduledIo instances of this state; in particular, notification of outstanding ScheduledIo registrations was driven by `Driver::drop`, but registrations bypass `Driver` and go directly to a `Weak<Inner>`. The `Driver` holds the `Arc<Inner>` keeping `Inner` alive, but it's possible that a new handle could be registered (or a new readiness future created for an existing handle) after the `Driver::drop` handler runs and prior to `Inner` being dropped. This change fixes this in two parts: First, notification of outstanding ScheduledIo handles is pushed down into the drop method of `Inner` instead, and, second, we add state to ScheduledIo to ensure that we remember that the IO driver we're bound to has shut down after the initial shutdown notification, so that subsequent readiness future registrations can immediately return (instead of potentially blocking indefinitely). Fixes: tokio-rs#2924
Implemented the owned version, and also ported TcpListener over as a proof-of-concept. In the process I rediscovered and resolved #2924 (I only realized there was already a PR after writing it, oops - but there's a second race in there as well) |
Oh. Well. I guess we can't replace PollEvented with AsyncFd :( |
I'll write up an independent test for the shutdown notification issue and remove the tcplistener port tomorrow. |
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.
This looks great 👍 thanks for taking this on.
I have some minor change requests, but we're almost there.
@seanmonstar Could you look at this? You worked in here most recently. |
/// The ownership of this slab is moved into this structure during | ||
/// `Driver::drop`, so that `Inner::drop` can notify all outstanding handles | ||
/// without risking new ones being registered in the meantime. | ||
resources: Mutex<Option<Slab<ScheduledIo>>>, |
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.
You have probably thought about this a lot more than me, but I fail to understand how would moving the slab into the Option ensure that no new resources are registered while dropping. Registration is happening through the slab::Allocator<ScheduledIo>
not the Slab, right?
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.
Moving it into Inner
isn't what ensures no further resources are registered. Because Handle
holds a weak ref to Inner
, we know that, when Inner
drops, no further registration can happen. So cleanup happens at that point in Inner::drop
.
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.
I do think there probably is a better way to handle shutdown, but this is the quickest way to correct the issues that exist today w/o a bigger refactor. Impact is minimal as well as the Mutex
is only used once in the lifecycle of the I/O driver (shutdown).
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.
God it, that makes sense! Thank you. Out of curiosity, what would that better way look like?
impl Drop for Inner { | ||
fn drop(&mut self) { | ||
let resources = self.resources.lock().take(); | ||
|
||
if let Some(mut slab) = resources { | ||
slab.for_each(|io| { | ||
// If a task is waiting on the I/O resource, notify it. The task | ||
// will then attempt to use the I/O resource and fail due to the | ||
// driver being shutdown. | ||
io.shutdown(); | ||
}); | ||
} |
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.
I think that changes things a bit. Before the idea was that the moment the driver gets dropped, it wakes all io tasks. Now however, there might be strong arc(s) being held somewhere else. So this logic is being delayed until all these arcs are dropped. Is that desirable? Does that actually solve the original racing issue? Might be missing something here...
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.
It should not change things materially. The strong refs are only held for small amounts of time to perform registration / deregistration.
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.
It's possible to keep the Inner alive with enough traffic on the handle, but this isn't worse than it was before (it's better, since eventually once all the Arc cloning stops and the Inner can be dropped, all the new handles that were registered after the driver was dropped will be properly notified)
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.
👍
This adds AsyncFd, a unix-only structure to allow for read/writability states
to be monitored for arbitrary file descriptors.
Issue: #2728
Motivation
See #2728 for the motivation for this change.
Solution
This introduces a new
AsyncFd
type which can be used to monitor arbitrary file descriptors for readability and writability. I've chosen not to expose apoll_*
API as callers can emulate such an API on their own (an example is given in the rustdocs). I've also avoided exposing specificReady
/Interest
-like types (this could be added in the future in a backwards-compatible way).I'm a little bit uncomfortable with how the cfg-macros work out here - I had to touch quite a few places to add the
async-fd
feature flag. That feels like a refactor for another PR however.