-
-
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
task: fix LocalSet having a single shared task budget #2462
Conversation
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
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.
Just a heads up there are also a few dbg!
s left in this change
agh! thanks for noticing! |
Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Hmm, it's definitely a little weird for Is there a reason to prefer this way of fixing it over, say, using |
Hmm. It also seemed wrong, to me, for all tasks on the |
Yeah, I honestly don't know what the right thing to do is here. My instinct is that If we were to go the route this PR proposes, we would at the very least need to special-case |
As an example of the kind of scenario I was concerned about, consider a case where I've spawned, say, a third of my futures on a When the threadpool scheduler is used, no tasks other than the one passed to And, it's worth remembering that the local set already has its own limit on the number of tasks it will poll per tick before yielding, separately from the task budget — if more than that number of tasks have been notified since the last local set tick, it will return I wonder if @carllerche has an opinion? |
Hmm, that's interesting. The observation that In light of ^, I think I agree with you that unconstrained budgeting probably makes sense for |
Motivation
Currently, an issue exists where a
LocalSet
has a single cooperativetask budget that's shared across all futures spawned on the
LocalSet
and by any future passed to
LocalSet::run_until
orLocalSet::block_on
. Because these methods will poll therun_until
future before polling spawned tasks, it is possible for that task to
always deterministically starve the entire
LocalSet
so that no localtasks can proceed. When the completion of that future itself depends
on other tasks on the
LocalSet
, this will then result in a deadlock,as in issue #2460.
A detailed description of why this is the case, taken from this
comment:
LocalSet
wraps each time a local task is run inbudget
:tokio/tokio/src/task/local.rs
Line 406 in 947045b
This is identical to what tokio's other schedulers do when running
tasks, and in theory should give each task its own budget every time
it's polled.
However,
LocalSet
is different from other schedulers. Unlike theruntime schedulers, a
LocalSet
is itself a future that's run onanother scheduler, in
block_on
.block_on
also sets a budget:tokio/tokio/src/runtime/basic_scheduler.rs
Line 131 in 947045b
The docs for
budget
state that:tokio/tokio/src/coop.rs
Line 73 in 947045b
This means that inside of a
LocalSet
, the calls tobudget
areno-ops. Instead, each future polled by the
LocalSet
is subtractingfrom a single global budget.
LocalSet
'sRunUntil
future polls the provided future before pollingany other tasks spawned on the local set:
tokio/tokio/src/task/local.rs
Lines 525 to 535 in 947045b
In this case, the provided future is
JoinAll
. Unfortunately, everytime a
JoinAll
is polled, it polls every joined future that has notyet completed. When the number of futures in the
JoinAll
is >= 128,this means that the
JoinAll
immediately exhausts the task budget. Thiswould, in theory, be a good thing --- if the
JoinAll
had a hugenumber of
JoinHandle
s in it and none of them are ready, it would limitthe time we spend polling those join handles.
However, because the
LocalSet
actually has a single shared taskbudget, this means polling the
JoinAll
always exhausts the entirebudget. There is now no budget remaining to poll any other tasks spawned
on the
LocalSet
, and they are never able to complete.Solution
This branch solves this issue by resetting the task budget when polling
a
LocalSet
. I've added a new function tocoop
for resetting the taskbudget to
UNCONSTRAINED
for the duration of a closure, and thusallowing the
budget
calls inLocalSet
to actually create a newbudget for each spawned local task. Additionally, I've changed
LocalSet
to also ensure that a separate task budget is applied toany future passed to
block_on
/run_until
.Additionally, I've added a test reproducing the issue described in
#2460. This test fails prior to this change, and passes after it.
Fixes #2460