-
-
Notifications
You must be signed in to change notification settings - Fork 346
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
Give a better error for improper combinations of AsyncExitStack with nurseries #1243
Comments
It's not so much about tasks as about stack discipline -- you're doing
but Trio expects you to exit nurseries in the opposite order you entered them, like you would if you'd used If I understand your goal correctly, you could do something like
The |
I agree that this is sloppy code (and probably conceptually wrong), but I don't think it's only about the incorrect nesting of the nurseries. For instance, the following example produces a similar trace even though the nurseries are correctly nested: async def start_context(stack, task_status):
nursery = await stack.enter_async_context(trio.open_nursery())
task_status.started(nursery)
async def main():
async with trio.open_nursery() as nursery:
async with AsyncExitStack() as stack:
context = await nursery.start(start_context, stack)
context.start_soon(trio.sleep, 1) Here's a more realistic example, showing how one might run into this issue without really noticing the sloppy logic. Obviously, the culprit here is: async def get_service(self, n):
if n not in self.cache:
self.cache[n] = await stack.enter_async_context(some_service())
return self.cache[n] That's when I realized that async def get_service(self, n):
async def target(task_status):
async with some_service() as service:
task_status.started(service)
await trio.sleep_forever()
if n not in self.cache:
self.cache[n] = await nursery.start(target)
return self.cache[n] (which is very similar to the example you provided). I don't know if this is a good reason to discourage the use of |
The problem isn't using a nursery vs. an exit stack to manage exits, the problem is that you're trying to use an exit stack to manage a nursery. That's possible, no problem, but you need to create the nursery within an This code works:
|
Oh yes, it works as long as async def main():
async with trio.open_nursery() as nursery:
async with AsyncExitStack() as stack:
async def target():
context = await stack.enter_async_context(start_context())
context.start_soon(trio.sleep, 1)
nursery.start_soon(target)
await trio.sleep(.1)
Maybe trio could provide its own enhanced version of |
I don't have any problem passing Yes, I learned the hard way that you can't play games with nursery enter and exits. Exits need to be in stack order and from the same task. You can usually structure your concurrency around this limitation. |
The problem isn't that you're doing it in another task. The problem is that you're dismantling things in some random order, i.e. not reversing the order you've set them up in. Don't do that. Nurseries go splat in a somewhat spectacular way when you do it, because they intrinsically depend on that order, but you can construct the same problem with any other pair of resources where one depends on the other. |
Yeah, there's two separate constraints: you have to exit nurseries in the same order you entered them, and you have to exit nurseries from the same task where you entered them. Basically nurseries are designed around
I mean, that would be possible to do. It would also be possible to get rid of nurseries altogether, and just let you spawn tasks whenever you wanted. It would be even easier than implementing nurseries, in fact :-). But the reason trio has nurseries is because we think that the constraint to use |
Alright, that makes sense!
Oh sorry I wrote this part to quickly, that's not what I meant. I just wondered it it was possible to check the constraints you mentioned in advance in order to raise an exception earlier, since it is easy to use async with AsyncExitStack() as stack:
async with trio.open_nursery() as nursery:
service = await stack.enter_async_context(
some_service_that_might_run_a_nursery()) This might seem obvious with small examples, but it very easy to miss in larger code base. In our case, we used
And that's much appreciated by the way :) |
Hmm, that does sound nice, but I'm not sure what constraints we could enforce. In your case, it sounds like the code was actually correct until that second nursery got added? Do you have a heuristic in mind for how we could detect that your usage was risky, before it actually became a problem? |
This is roughly what I had in mind, although I might be missing something: import trio
import contextlib
class AsyncExitStack(contextlib.AsyncExitStack):
async def __aenter__(self):
self._task = trio.hazmat.current_task()
self._current_scope = self._task._cancel_status._scope
return await super().__aenter__()
async def enter_async_context(self, cm):
if self._task != trio.hazmat.current_task():
raise RuntimeError(
"Async context must be entered in the same task as the stack"
)
if self._current_scope != self._task._cancel_status._scope:
raise RuntimeError(
"Async context must be entered in the scope of the previous context"
)
try:
return await super().enter_async_context(cm)
finally:
self._current_scope = self._task._cancel_status._scope The It seems to work as expected: import pytest
@pytest.mark.trio
async def test_async_exit_stack():
# Proper nesting and single task
async with trio.open_nursery() as nursery:
async with AsyncExitStack() as stack:
service1 = await stack.enter_async_context(trio.open_nursery())
service2 = await stack.enter_async_context(trio.open_nursery())
nursery.start_soon(trio.sleep, 0.1)
service1.start_soon(trio.sleep, 0.1)
service2.start_soon(trio.sleep, 0.1)
await trio.sleep(0.01)
# Invalid nesting
async with AsyncExitStack() as stack:
async with trio.open_nursery() as nursery:
with pytest.raises(RuntimeError):
await stack.enter_async_context(trio.open_nursery())
# Multiple tasks
async with trio.open_nursery() as nursery:
async with AsyncExitStack() as stack:
async def target():
with pytest.raises(RuntimeError):
await stack.enter_async_context(trio.open_nursery())
nursery.start_soon(target)
await trio.sleep(0.01) |
It's way more likely for an application to be doing something wrong with AsyncExitStack or explicit aenter/aexit calls than for there to be a Trio bug. The message here should at least reflect that. If it's possible to detect the issue slightly higher up in the stack and give a little more context in the error, that would be even better. The error which comes up when calling yield from a nursery should get similar treatment. While it does suggest your application is probably doing something wrong, it should mention the nursery/generator limitation explicitly. |
Hi all, I ran into a weird issue today. Consider the following program:
It opens an
AsyncExitStack
and uses it to enter a nursery from another task. This produces the following traceback:I suspect this issue to be caused by the fact the the nursery is not entered and exited in the same task. As far as I understand this is a trio limitation. In this case, a nicer exception could really help :)
The text was updated successfully, but these errors were encountered: