-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(tokio/macros): add biased mode to join! and try_join! #7307
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
base: master
Are you sure you want to change the base?
Conversation
Here's the PR related to our discussion yesterday @maminrayej - thanks for the guidance! |
tokio/src/macros/try_join.rs
Outdated
let mut skip_next_time: u32 = 0; | ||
let rotate_skip = $rotate_poll_order; |
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 would like to avoid having this variable at all when using biased. I don't think it can be optimized out because it gets stored as a field in the closure type.
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.
My newest commit removes this variable, along with skip_next_time
, from the biased branch's poll_fn()
(so it will now be smaller than the unbiased equivalent).
It's a bit verbose... since each branch of match $rotate_poll_order {}
needs to generate its own unnameable poll function. I initially was pulling out common logic into a closure, but I felt it was ultimately less readable.
Glad to take another stab at simplifying if you have any feedback.
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.
Yeah, I don't think we want the match. It duplicates the entire logic, which isn't nice. One option could be to define two helper types:
#[derive(Default)]
pub struct Rotater<const COUNT: usize> {
next: usize,
}
impl<const COUNT: usize> Rotater<COUNT> {
#[inline]
pub fn num_skip(&mut self) -> usize {
let mut num_skip = self.next;
self.next += 1;
if self.next == COUNT {
self.next = 0;
}
num_skip
}
}
#[derive(Default)]
pub struct BiasedRotater {}
impl BiasedRotater {
#[inline]
pub fn num_skip(&mut self) -> usize {
0
}
}
and then have the macro select which type to use:
( biased; $($e:expr),+ $(,)?) => {
$crate::join!(@{ rotator=$crate::macros::support::BiasedRotator; () (0) } $($e,)*)
};
( $($e:expr),+ $(,)?) => {
$crate::join!(@{ rotator=$crate::macros::support::Rotator; () (0) } $($e,)*)
};
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.
Ah, that's clever. Thanks for the advice! Will take a crack at it.
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.
Got it working, thanks!
Appreciate the eyes, Darksonn@ - I'll take a stab at implementing the requested feedback. |
@Darksonn latest refactor is up. I noticed our existing |
tokio/src/macros/support.rs
Outdated
@@ -28,3 +28,34 @@ cfg_macros! { | |||
pub use std::future::{Future, IntoFuture}; | |||
pub use std::pin::Pin; | |||
pub use std::task::{Context, Poll}; | |||
|
|||
#[doc(hidden)] |
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 entire module is #[doc(hidden)]
already so you don't need this.
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, will update.
tokio/src/macros/support.rs
Outdated
|
||
#[doc(hidden)] | ||
#[derive(Default, Debug)] | ||
pub struct Rotator<const COUNT: u32> { |
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.
Can we make it clear that these types are here for use with the join!
macro? In fact, maybe move them to src/macros/join.rs
and re-export them 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.
Sure, makes sense. Will do.
tokio/src/macros/try_join.rs
Outdated
/// let res = tokio::try_join!( | ||
/// //do_stuff_async() will always be polled first when woken | ||
/// biased; | ||
/// do_stuff_async(), | ||
/// more_async_work()); |
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 confusing that you have a comment inside the macro before biased;
. Also, please format this code properly by moving the );
to its own line.
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.
Will do on both counts. Sorry, was overly reliant on rustfmt
here, will manually clean up.
tokio/src/macros/try_join.rs
Outdated
loop { | ||
$( | ||
if skip == 0 { | ||
if to_run == 0 { | ||
// Every future has been polled | ||
break; | ||
} | ||
to_run -= 1; | ||
$( | ||
if skip == 0 { | ||
if to_run == 0 { | ||
// Every future has been polled | ||
break; | ||
} | ||
to_run -= 1; | ||
|
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 the indentation changed throughout the entire macro?
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.
Will tweak, was overly reliant on rustfmt
which has trouble with macros I guess.
}; | ||
|
||
tokio::join!( | ||
tokio::try_join!( |
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 was a mistake in existing test, i think
…emove comment inside macro invocation in example
@Darksonn latest changes are up per your comments, thanks for your time on this PR review. |
tokio/src/macros/support.rs
Outdated
// Used with `join!` and `try_join!` | ||
pub use crate::macros::join::Rotator; | ||
// Used with `join!` and `try_join!` | ||
pub use crate::macros::join::BiasedRotator; |
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.
// Used with `join!` and `try_join!` | |
pub use crate::macros::join::Rotator; | |
// Used with `join!` and `try_join!` | |
pub use crate::macros::join::BiasedRotator; | |
// Used with `join!` and `try_join!` | |
pub use crate::macros::join::{BiasedRotator, Rotator}; |
tokio/src/macros/support.rs
Outdated
|
||
|
||
} |
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.
Unnecessary empty lines.
tokio/src/macros/join.rs
Outdated
#[derive(Default, Debug)] | ||
/// Rotates by one every [`Self::num_skip`] call up to COUNT - 1. | ||
pub struct Rotator<const COUNT: u32> { |
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.
#[derive(Default, Debug)] | |
/// Rotates by one every [`Self::num_skip`] call up to COUNT - 1. | |
pub struct Rotator<const COUNT: u32> { | |
/// Rotates by one every [`Self::num_skip`] call up to COUNT - 1. | |
#[derive(Default, Debug)] | |
pub struct Rotator<const COUNT: u32> { |
/// But there is an important caveat to this mode. It becomes your responsibility | ||
/// to ensure that the polling order of your futures is fair. If for example you | ||
/// are joining a stream and a shutdown future, and the stream has a | ||
/// huge volume of messages that takes a long time to finish processing per poll, you should | ||
/// place the shutdown future earlier in the `join!` list to ensure that it is | ||
/// always polled, and will not be delayed due to the stream future taking a long time to return | ||
/// `Poll::Pending`. |
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 don't think that a shutdown future is an amazing example here. That makes sense for select!
, but you wouldn't really have a shutdown future in join!
. Not sure what a better example would be, though.
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.
Here is a real-world example usage involving a shutdown signal: https://github.com/awslabs/aws-lambda-rust-runtime/blob/main/lambda-runtime/src/lib.rs#L227
The distinction here is that the other task is a server future, not a stream future, meaning it will generally keep running and generating new request futures. So we need to continue driving it. Even if the shutdown future will only ever come up once and then resolve, we still need to check it first every time.
I guess technically the server future will generally be dispatching work to read from sockets rather than processing the messages directly (ie not long polls as a concern), but anyway you want to shut down before dispatching that work.
Anyway I'll tweak wording from stream -> server and finesse it a bit more.
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.
Also renaming the section Fairness
-> Poll Ordering
, it's not really about fairness anymore in the example given.
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.
Actually, I changed it back. My initial rework was basically restating that deterministic ordering is useful. But it's important for readers to consider the case that one future might take a long time to poll and starve the other, that was the point of the fairness discussion.
Based on the above lambda runtime link, I do think there are cases where you would be joining a shutdown future, so it's probably still relevant? Anyway I think it does a good job of illustrating the fairness risk even if the specific use case isn't the most universal?
Glad to keep thinking about it though if not sold and try to come up with another example.
…ng/import organization
Closes: #7304
Adding a
biased;
mode to thejoin!
andtry_join!
macros that always polls the wrapped futures in their order of declaration, rather than rotating each poll.This parallels the
select!
API (though is simpler under the hood, since these macros rotate rather than randomize which branch is polled first).Motivation
This is useful when the contained futures interact in a way where polling order is significant.
My specific case is that I want to
join!
ortry_join!
two separate listener futures in an AWS Lambda. Lambdas receive one request at a time. One of my listeners is my primary function handler, which is higher priority. The second is an extension that does post-request 'telemetry flushing'. So, there is never a need for the second future to be polled first.Here is a rough example: ref.
Solution
I added an optional
biased;
token to each macro's entrypoint. This resolves to a literal variable,$rotate_poll_order
, as eithertrue
orfalse
(with nobiased;
declaration resulting intrue
).Then, inside the generated code, we only increment the skip counter used to rotate polling, if
$rotate_poll_order
istrue
.I initially changed the skip order counter from
u32
toOption<u32>
, but that would have resulted in a bit muddier code. Adding an if clause around the single line incrementing the skip counter, was more readable and kept the footprint of this change more contained.The performance impact of this change on cases that do NOT use the
biased
mode is:This perf impact seems very marginal, but we could always make a more complicated macro that generates distinct futures based on bias mode. Probably the external API should stay the same as
select!
though (ie, just add thebiased;
token to the existing macros).