-
-
Notifications
You must be signed in to change notification settings - Fork 335
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
implement open_nursery with explicit context manager #612
Conversation
|
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.
Haven't looked at the tests yet because I'm on my phone, but two quick comments I noticed.
trio/_core/_run.py
Outdated
# | ||
# def open_nursery(): | ||
# return NurseryManager() | ||
nested_child_exc = exc if isinstance(exc, BaseException) else None |
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 line is equivalent to nested_child_exc = exc
:-). The context manager protocol says that exc
can be either an exception object, or None
. And all exception objects are instances of BaseException
, by definition.
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.
done
trio/_core/_run.py
Outdated
def __enter__(self): | ||
raise RuntimeError( | ||
"use 'async with {func_name}(...)', not 'with {func_name}(...)'". | ||
format(func_name=self._wrapper_func_name) |
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 wrapper func name is always open_nursery
, so it can be hard coded 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.
done
53e935b
to
0bc12ef
Compare
Regarding the two failing tests, it's clearly regarding the case of nursery being cancelled before a start task can respond with
My tentative conclusion is that this can't be solved in NurseryManager; rather Nursery is needing some extra logic. update: has something to do with the system nursery's entry_queue. The entry_queue.spawn() task also raises Cancelled for the case in question, and the cancel scope appears to not be properly suppressing the exception --> |
Just pushed a fix. How I tracked it down: I looked at the two failing tests. The one testing async with trio.open_nursery() as nursery:
async with stream:
nursery.cancel_scope.cancel()
# The dedent here exits the 'async with stream' block, which calls 'await stream.aclose()'
# This call raises 'Cancelled' (as it should, since we just cancelled the surrounding scope).
# And then this exception escapes the nursery block, which is the bug. So I tried writing a minimal version of this: import trio
async def main():
async with trio.open_nursery() as nursery:
nursery.cancel_scope.cancel()
await trio.sleep(0)
trio.run(main) and indeed, this crashes the same way. So now we have a minimal reproducer. I still had absolutely no idea what was going on though. How come this Cancelled exception wasn't being caught by the cancel scope? So next I stuck a pdb breakpoint at the top of So I squinted hard at |
Codecov Report
@@ Coverage Diff @@
## master #612 +/- ##
==========================================
+ Coverage 99.31% 99.33% +0.01%
==========================================
Files 91 91
Lines 10800 11073 +273
Branches 751 816 +65
==========================================
+ Hits 10726 10999 +273
Misses 56 56
Partials 18 18
Continue to review full report at Codecov.
|
@njsmith thank for the fix and debug tips! I lost most of a day on this-- yes unfortunately dug into the more complex of the two failing tests. How important is it to not explicitly re-raise if |
I'm actually not sure. I stole this from the implementation of I suppose it would be easy to write a test that hits that branch, just something like: with pytest.raises(KeyError):
async with trio.open_nursery() as nursery:
raise KeyError() Maybe we should do that anyway... |
Did a little experiment: class CM:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_tb):
# <insert code here>
with CM():
raise KeyError When Traceback (most recent call last):
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
KeyError When Traceback (most recent call last):
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
File "/tmp/asdf.py", line 6, in __exit__
raise v
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
KeyError When Traceback (most recent call last):
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
KeyError So I suppose the next step in reducing traceback clutter will be to refactor |
@njsmith hitting that Actually the exception comes from the cancel scope |
@njsmith So we can put an additional try-catch on the cancel scope |
04ad88c
to
3ce95ef
Compare
Okay, yeah, I see it. Wow, this is complicated. It's looking like #607 will involve refactoring cancel scope entry/exit to use the context manager protocol instead of |
Before/after using the same trace I referenced in #56 (comment) As a user I'd much prefer the general eliding I posted there, though I understand that this work clears some other bugs. Before (39 frames): Traceback (most recent call last):
File "demo.py", line 58, in <module>
test_multiplexer_with_error()
File "demo.py", line 55, in test_multiplexer_with_error
trio.run(runner2)
File "/.../site-packages/trio/_core/_run.py", line 1277, in run
return result.unwrap()
File "/.../site-packages/outcome/_sync.py", line 107, in unwrap
raise self.error
File "/.../site-packages/trio/_core/_run.py", line 1387, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "/.../site-packages/trio/_core/_run.py", line 970, in init
self.entry_queue.spawn()
File "/.../site-packages/async_generator/_util.py", line 42, in __aexit__
await self._agen.asend(None)
File "/.../site-packages/async_generator/_impl.py", line 366, in step
return await ANextIter(self._it, start_fn, *args)
File "/.../site-packages/async_generator/_impl.py", line 202, in send
return self._invoke(self._it.send, value)
File "/.../site-packages/async_generator/_impl.py", line 209, in _invoke
result = fn(*args)
File "/.../site-packages/trio/_core/_run.py", line 317, in open_nursery
await nursery._nested_child_finished(nested_child_exc)
File "/.../contextlib.py", line 99, in __exit__
self.gen.throw(type, value, traceback)
File "/.../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope
yield scope
File "/.../site-packages/trio/_core/_multierror.py", line 144, in __exit__
raise filtered_exc
File "/.../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope
yield scope
File "/.../site-packages/trio/_core/_run.py", line 317, in open_nursery
await nursery._nested_child_finished(nested_child_exc)
File "/.../site-packages/trio/_core/_run.py", line 428, in _nested_child_finished
raise MultiError(self._pending_excs)
File "/.../site-packages/trio/_core/_run.py", line 1387, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "demo.py", line 52, in runner2
nursery.start_soon(writer2, mx, (7,9))
File "/.../site-packages/async_generator/_util.py", line 42, in __aexit__
await self._agen.asend(None)
File "/.../site-packages/async_generator/_impl.py", line 366, in step
return await ANextIter(self._it, start_fn, *args)
File "/.../site-packages/async_generator/_impl.py", line 202, in send
return self._invoke(self._it.send, value)
File "/.../site-packages/async_generator/_impl.py", line 209, in _invoke
result = fn(*args)
File "/.../site-packages/trio/_core/_run.py", line 317, in open_nursery
await nursery._nested_child_finished(nested_child_exc)
File "/.../contextlib.py", line 99, in __exit__
self.gen.throw(type, value, traceback)
File "/.../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope
yield scope
File "/.../site-packages/trio/_core/_multierror.py", line 144, in __exit__
raise filtered_exc
File "/.../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope
yield scope
File "/.../site-packages/trio/_core/_run.py", line 317, in open_nursery
await nursery._nested_child_finished(nested_child_exc)
File "/.../site-packages/trio/_core/_run.py", line 428, in _nested_child_finished
raise MultiError(self._pending_excs)
File "/.../site-packages/trio/_core/_run.py", line 1387, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "demo.py", line 15, in reader
raise e
File "demo.py", line 9, in reader
value = await mx[key]
File "multiplexer.py", line 41, in __getitem__
value = await trio.hazmat.wait_task_rescheduled(abort_fn)
File "/.../site-packages/trio/_core/_traps.py", line 159, in wait_task_rescheduled
return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap()
File "/.../site-packages/outcome/_sync.py", line 107, in unwrap
raise self.error
Exception: Ka-Boom! After (31 frames, -8): Traceback (most recent call last):
File "demo.py", line 58, in <module>
test_multiplexer_with_error()
File "demo.py", line 55, in test_multiplexer_with_error
trio.run(runner2)
File "/.../site_packages/trio/_core/_run.py", line 1264, in run
return result.unwrap()
File "/.../site-packages/outcome/_sync.py", line 107, in unwrap
raise self.error
File "/.../site_packages/trio/_core/_run.py", line 1374, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "/.../site_packages/trio/_core/_run.py", line 957, in init
self.entry_queue.spawn()
File "/.../site_packages/trio/_core/_run.py", line 313, in __aexit__
type(new_exc), new_exc, new_exc.__traceback__
File "/.../contextlib.py", line 99, in __exit__
self.gen.throw(type, value, traceback)
File "/.../site_packages/trio/_core/_run.py", line 200, in open_cancel_scope
yield scope
File "/.../site_packages/trio/_core/_multierror.py", line 144, in __exit__
raise filtered_exc
File "/.../site_packages/trio/_core/_run.py", line 200, in open_cancel_scope
yield scope
File "/.../site_packages/trio/_core/_run.py", line 309, in __aexit__
await self._nursery._nested_child_finished(exc)
File "/.../site_packages/trio/_core/_run.py", line 415, in _nested_child_finished
raise MultiError(self._pending_excs)
File "/.../site_packages/trio/_core/_run.py", line 1374, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "demo.py", line 52, in runner2
nursery.start_soon(writer2, mx, (7,9))
File "/.../site_packages/trio/_core/_run.py", line 313, in __aexit__
type(new_exc), new_exc, new_exc.__traceback__
File "/.../contextlib.py", line 99, in __exit__
self.gen.throw(type, value, traceback)
File "/.../site_packages/trio/_core/_run.py", line 200, in open_cancel_scope
yield scope
File "/.../site_packages/trio/_core/_multierror.py", line 144, in __exit__
raise filtered_exc
File "/.../site_packages/trio/_core/_run.py", line 200, in open_cancel_scope
yield scope
File "/.../site_packages/trio/_core/_run.py", line 309, in __aexit__
await self._nursery._nested_child_finished(exc)
File "/.../site_packages/trio/_core/_run.py", line 415, in _nested_child_finished
raise MultiError(self._pending_excs)
File "/.../site_packages/trio/_core/_run.py", line 1374, in run_impl
msg = task.context.run(task.coro.send, next_send)
File "/.../site-packages/contextvars/__init__.py", line 38, in run
return callable(*args, **kwargs)
File "demo.py", line 15, in reader
raise e
File "demo.py", line 9, in reader
value = await mx[key]
File "multiplexer.py", line 41, in __getitem__
value = await trio.hazmat.wait_task_rescheduled(abort_fn)
File "/.../site_packages/trio/_core/_traps.py", line 159, in wait_task_rescheduled
return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap()
File "/.../site-packages/outcome/_sync.py", line 107, in unwrap
raise self.error
Exception: Ka-Boom! |
@njsmith ready to go I think Let me know if you'd use merge, squash, or rebase in this case. |
I'm usually lazy and just merge...
I agree! Let's do this. |
Just wanted to respond to a comment above:
This made me go back and read some documentation on
I hope this is helpful. |
avoid use of
@asynccontextmanager
and@async_generator
since they cause bugs as well as extraneous exception stack framesTODO:
if new_exc is exc
case)