python-trio/trio

MultiError sad story

Closed this issue · 2 comments

belm0 commented

I know there are plans to have nursery raise MultiError exclusively, but I'll share this story.

A nursery is wrapped in a try/catch naively catching a bare exception. Works fine, except when an enclosing move_on_after() happens to reach its deadline simultaneously with an exception within the nursery.

What made it so hard to debug is that the final exception trace has no mention of MultiError, only the bare exception. Which made it seem like the except clause was mysteriously ignored.

Here is the repro I used to understand this:

from random import uniform
import trio

count = 0

async def raiser():
    await trio.sleep(30 / 1000)
    raise ValueError('foo')

async def async_main():
    global count
    while True:
        count += 1
        with trio.move_on_after(uniform(20, 40) / 1000):
            try:
                # The following nursery will sometimes leak a ValueError
                # despite the try/except.
                async with trio.open_nursery() as nursery:
                    nursery.start_soon(raiser)
                    await trio.sleep_forever()
                # If replace with a direct call to raiser() it doesn't occur.
                #await raiser()
            except ValueError:
                pass
            except BaseException as e:
                print(type(e), e)
                raise

try:
    trio.run(async_main)
except KeyboardInterrupt:
    pass
finally:
    print('count:', count)
<class 'trio.MultiError'> Cancelled(), Cancelled(), Cancelled()
<class 'trio.MultiError'> Cancelled(), Cancelled(), Cancelled()
...
<class 'trio.MultiError'> ValueError('foo',), Cancelled()
count: 29
Traceback (most recent call last):
  File "main.py", line 30, in <module>
    trio.run(async_main)
  File "/.../site-packages/trio/_core/_run.py", line 1337, in run
    raise runner.main_task_outcome.error
  File "main.py", line 27, in async_main
    raise
  File "/.../site-packages/trio/_core/_ki.py", line 165, in wrapper
    return fn(*args, **kwargs)
  File "/.../site-packages/trio/_core/_run.py", line 260, in __exit__
    raise remaining_error_after_cancel_scope
  File "main.py", line 20, in async_main
    await trio.sleep_forever()
  File "/.../site-packages/trio/_core/_run.py", line 397, in __aexit__
    raise combined_error_from_nursery
  File "main.py", line 8, in raiser
    raise ValueError('foo')
ValueError: foo

(FYI @belm0 you can syntax-highlight code blocks for python and python-traceback)

Hmm, yeah, it sounds like this is exactly the kind situation that MultiError v2 should help with. So far though it's been motivated by theoretical arguments; I think this is the first report we've had of someone hitting it in the real world. Sorry you hit that, it sounds unpleasant! But thank you for writing it up, it's a compelling argument that we're on the right track with #611 :-).

Since it sounds like the solution here is already included in #611, I'm going to close this and link to it there.