implement open_nursery with explicit context manager#612
implement open_nursery with explicit context manager#612njsmith merged 10 commits intopython-trio:masterfrom
Conversation
|
|
njsmith
left a comment
There was a problem hiding this comment.
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.
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.
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.
The wrapper func name is always open_nursery, so it can be hard coded here.
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 KeyErrorWhen Traceback (most recent call last):
File "/tmp/asdf.py", line 9, in <module>
raise KeyError
KeyErrorWhen 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
KeyErrorWhen 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
KeyErrorSo 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
@asynccontextmanagerand@async_generatorsince they cause bugs as well as extraneous exception stack framesTODO:
if new_exc is exccase)