Skip to content
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

Iteration policy API for eager polling loops #1961

Closed
wants to merge 44 commits into from

Conversation

mzabaluev
Copy link
Contributor

Work to fix #1957 and provide a new macro for responsible polling.

@mzabaluev
Copy link
Contributor Author

This is a simple all-dynamic solution that augments futures and streams which may be affected by saturation of their poll function with a counter-based relief valve.

A more elaborate, generic extension is also possible that will allow the users to opt out of the iteration counter statically, which will be backward compatible with these additions (except the poll_loop! macro that would need to make use of a protocol trait to be future proof). But that may be overkilling it. If I get around to do it, I will submit the generic solution in another PR.

@mzabaluev
Copy link
Contributor Author

I'm not sure about landing the iteration API and the poll_loop! macro in futures-core. These are implementation helpers, not essential definitions, and so they could be moved to futures-util or even to their own dedicated crate if deemed necessary.

@mzabaluev mzabaluev changed the title Busy looping guards for Stream-consuming futures Iteration policy API for eager polling loops Nov 18, 2019
Starting work on adding busy loop guards to stream combinators.
An unlabeled break expression inside the loop body given to poll_loop!
breaks out of the loop in the expansion with the effect of yielding
to the task. Make the users conscious of this by requiring the break
expression to carry a token value of type $crate::task::ToYield.
Import the poll_loop! macro from futures-core.

Define a constant with some uneducated guess value on a
good default for yielding after this number of iterations, which
should keep the overhead low in good cases, but not block
the outer poll for too many iterations.
Not normally expected to occur, but a busy stream of empty streams
can theoretically saturate this loop.
These can busy loop if the filter future consistenly filters out items.
Use NonNullU32 for the limits on the number of iterations to give
the optimizer information that the first iteration will always be taken,
allowing to unroll it or move the loop exit check to the end.

To make poll_loop! macro work with a NonNullU32 argument,
apply an Into<u32> conversion to it. The iteration range is now
strictly in u32, while previously the integer type was inferred from
the macro's argument.
Add combinator method .yield_after_every(n) to stream and future
combinator types that have been provided with iteration guards.

This gives the user means to fine tune the yielding behavior.
Future-proofing the poll_loop! macro by defining a Policy
trait that is provided by poll guard implementations:
Limit with the maximum count, and Unlimited that does not do any checks.
Perform the yield check at the end of an iteration. Document
the check-skipping effect of `continue` and the new semantics of
`break`: this can now be used to return arbitrary values from
the poll_loop! expression. This is less semantically weird than
`break ToYield`, and `continue` gets back its usual meaning of
continuing iteration unconditionally.
iteration::Policy gets methods to observe how the loop has ended.
LoopGuard, a RAII helper, is used by poll_loop! to catch early returns.
The uses of poll_loop! in futures-util needed to be changed to split
borrows out of Pin<&mut Self>, allowing the guard to borrow the
iteration checker while other state objects are used and modified
by the loop body.
Notably, this makes the code of the loops more readable.
Add optional customization of a possibly nested path to the
iteration policy checker field.
The default limit is nearly arbitrary, aiming to keep the yielding
overhead in low single digit percentages while not starving other
pollables for too many iterations.
Making it a power of two does not make much difference,
except that it may produce more efficient allocation patterns
in hypothetical cases e.g. when a future accumulates a collection from
polled items, then yields, which causes draining of the collection
by some other pollable.
It's not a good idea to let impls run arbitrary code in the RAII
destructor which may be invoked in an unwind.
Also, separation between yield_check and when_yielded is not
really necessary. Erase when_* methods on Policy and allow the
yield_check method to modify the receiver so that adaptive and
instrumentation impls are still supported.

With this, LoopGuard is not needed in poll_loop! and is gone.
Renamed:
Policy -> LoopPolicy, for more instant legibility in docs
Policy::begin -> LoopPolicy::enter, clearer meaning
Make the meta parameter names describe what each doc string refers to.
Copy link
Member

@taiki-e taiki-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late review.

I would not prefer to provide abstractions or APIs like this on futures side for now. I also would not prefer to use the way of increasing the size of the combinators like this approach.
I'm not clear what the medium-term solution to this problem is (In the short-term, we will need to use an approach like #2333), but I think it will be resolved by std in the long-term.

So I'm going to close this PR. Thanks anyway @mzabaluev!

@taiki-e taiki-e closed this Feb 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ForEach, Fold, and similar stream combinators can run saturated without returning from poll
2 participants