-
-
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
Refactor IO registration using intrusive linked list #2828
Conversation
3dffee1
to
b26ed76
Compare
ee8a4b6
to
9ce4e61
Compare
2c4f851
to
1faa353
Compare
1faa353
to
7a43d9c
Compare
5533179
to
b321df3
Compare
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 great to me 👍 Great job. I appreciate keeping the PR as minimal as possible as well.
I had some super minor nits I posted inline, nothing is a blocker.
I would like to get another reviewer, especially on the unsafe bits. I tagged a few others on the PR.
@@ -150,11 +150,11 @@ jobs: | |||
run: cargo install cargo-hack | |||
|
|||
- name: check --each-feature | |||
run: cargo hack check --all --each-feature -Z avoid-dev-deps | |||
run: cargo hack check --all --each-feature --skip io-driver,io-readiness -Z avoid-dev-deps |
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.
Why is this needed?
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.
If only io-driver
or io-readiness
are enabled, it results in a bunch of unused warnings. The other option is to remove the features and tag all the related code with the cfg(any(..))
matrix of things that need the io-driver stuff.
tokio/src/sync/task/atomic_waker.rs
Outdated
#[cfg(feature = "io-driver")] | ||
#[allow(unused)] |
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 probably can delete it then.
It would definitely be nice to see a performance comparison --- I imagine this could make some cases faster but it would be nice to have some measurement rather than guesses. |
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 have not looked at the entire thing, but focused on the parts with unsafe.
return Poll::Pending; | ||
} | ||
|
||
// Explicit drop of the lock to indicate the scope that the | ||
// lock is held. Because holding the lock is required to | ||
// ensure safe access to fields not held within the lock, it | ||
// is helpful to visualize the scope of the critical | ||
// section. | ||
drop(waiters); |
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 code is correct, but the return
seems a bit confusing considering the comment.
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.
Which return
is confusing?
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.
Probably the return Poll::Pending
? I think it is ok as return
clearly indicates the mutex goes away.
// Safety: State::Done means it is no longer shared | ||
let w = unsafe { &mut *waiter.get() }; |
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 tried to verify this by following the code around, but found it non-obvious. It is still in the intrusive linked list as far as I can see?
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.
Inside ScheduledIo::wake
, at line 216, the waiters are drained out of the list when 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 looks good to me overall! I didn't see any obvious issues with the new implementation beyond a concern about leaving a pointer dangling if the mutex is poisoned.
|
||
impl Drop for Readiness<'_> { | ||
fn drop(&mut self) { | ||
let mut waiters = self.scheduled_io.waiters.lock().unwrap(); |
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.
since removing the waiter from the list is necessary to maintain safety invariants, i think we should change this to use LockResult::into_inner
to extract a mutex guard even if the lock was poisoned. if we don't ensure it's removed from the list, we'll leave the list with a potentially dangling pointer that is assumed to be valid.
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 idea of relying on Drop
for safety is scary and probably wrong...
Also, I borrowed a lot of this from notify
, like:
tokio/tokio/src/sync/notify.rs
Line 560 in e7091fd
let mut waiters = notify.waiters.lock().unwrap(); |
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 idea of relying on Drop for safety is scary and probably wrong...
Pin
guarantees that the memory location remains valid until it is dropped. If it is not dropped, then the memory must be leaked and remain valid forever.
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 idea of relying on
Drop
for safety is scary and probably wrong...
We're relying on the drop impl to maintain safety regardless of whether or not we ignore mutex poisoning here, it's inherent to the intrusive list that nodes must be unlinked before they are deallocated.
I think Notify
also needs to needs to ignore poisoning for the same reason. It might actually matter more in that case since there are probably going to be higher-level users of Notify
that send a notification when dropped, potentially including while unwinding...
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 this change should be done in a dedicated PR :) I tracked it here: #2867
For now, unwrap()
is fine.
Co-authored-by: Eliza Weisman <eliza@buoyant.io> Co-authored-by: Alice Ryhl <alice@ryhl.io>
ac42251
to
f823cf5
Compare
f823cf5
to
dbb99f3
Compare
Uses the infrastructure added by #2828 to enable switching `TcpListener::accept` to use `&self`. This also switches `poll_accept` to use `&self`. While doing introduces a hazard, `poll_*` style functions are considered low-level. Most users will use the `async fn` variants which are more misuse-resistant. TcpListener::incoming() is temporarily removed as it has the same problem as `TcpSocket::by_ref()` and will be implemented later.
Uses the infrastructure added by #2828 to enable switching `TcpListener::accept` to use `&self`. This also switches `poll_accept` to use `&self`. While doing introduces a hazard, `poll_*` style functions are considered low-level. Most users will use the `async fn` variants which are more misuse-resistant. TcpListener::incoming() is temporarily removed as it has the same problem as `TcpSocket::by_ref()` and will be implemented later.
This refactors IO registration in a few ways:
PollEvented
. This cache used to be helpful when readiness was a linked list of*mut Node
s inRegistration
. Previous refactors have turnedRegistration
into just anAtomicUsize
holding the current readiness, so the cache is just extra work and complexity. Gone.Registration
for readiness now gives aReadyEvent
, which includes the driver tick. This event must be passed back intoclear_readiness
, so that the readiness is only cleared fromRegistration
if the tick hasn't changed. Previously, it was possible to clear the readiness even though another thread had just polled the driver and found the socket ready again.async fn readiness
, which stores wakers in an instrusive linked list. This allows an unbounded number of tasks to register for readiness (previously, only 1 per direction (read and write)). By using the intrusive linked list, there is no concern of leaking the storage of the wakers, since they are stored inside theasync fn
and released when the future is dropped.poll_readiness(Direction)
method, to supportAsyncRead
andAsyncWrite
. They aren't able to useasync fn
s, and so there are 2 reserved slots for those methods.async fn readiness
, such asUdpSocket
andUnixDatagram
.Additionally, this makes the
io-driver
"feature" internal-only (no longer documented, not part of public API), and adds a second internal-only feature,io-readiness
, to group together linked list part of registration that is only used by some of the IO types.After a bit of discussion, changing stream-based transports (like
TcpStream
) to haveasync fn read(&self)
is punted, since that is likely too easy of a footgun to activate.cc #2779, #2728