On Mon, Apr 5, 2021 at 3:07 AM Nathaniel Smith <njs@pobox.com> wrote:
- Recording pre-empted exceptions: This is another type of metadata that would be useful to print along with the traceback. It's non-obvious and a bit hard to explain, but multiple trio users have complained about this, so I assume it will bite asyncio users too as soon as TaskGroups are added. The situation is, you have a parent task P and two child tasks C1 and C2:
P / \ C1 C2
C1 terminates with an unhandled exception E1, so in order to continue unwinding, the nursery/taskgroup in P cancels C2. But, C2 was itself in the middle of unwinding another, different exception E2 (so e.g. the cancellation arrived during a `finally` block). E2 gets replaced with a `Cancelled` exception whose __context__=E2, and that exception unwinds out of C2 and the nursery/taskgroup in P catches the `Cancelled` and discards it, then re-raises E1 so it can continue unwinding.
The problem here is that E2 gets "lost" -- there's no record of it in the final output. Basically E1 replaced it. And that can be bad: for example, if the two children are interacting with each other, then E2 might be the actual error that broke the program, and E1 is some exception complaining that the connection to C2 was lost. If you have two exceptions that are triggered from the same underlying event, it's a race which one survives.
This point reminded me again of this issue in the tracker ("Problems with recursive automatic exception chaining" from 2013): https://bugs.python.org/issue18861 I'm not sure if it's exactly the same, but you can see that a couple of the later comments there talk about "exception trees" and other types of annotations. If that issue were addressed after ExceptionGroups were introduced, does that mean there would then be two types of exception-related trees layered over each other (e.g. groups of trees, trees of groups, etc)? It makes me wonder if there's a more general tree structure that could accommodate both use cases... --Chris This is conceptually similar to the way an exception in an 'except' block
used to cause exceptions to be lost, so we added __context__ to avoid that. And just like for __context__, it would be nice if we could attach some info to E1 recording that E2 had happened and then got preempted. But I don't see how we can reuse __context__ itself for this, because it's a somewhat different relationship: __context__ means that an exception happened in the handler for another exception, while in this case you might have multiple preempted exceptions, and they're associated with particular points in the stack trace where the preemption occurred.
This is a complex issue and maybe we should call it out-of-scope for the first version of ExceptionGroups. But I mention it because it's a second place where adding some extra annotations to the traceback info would be useful, and maybe we can keep it simple by adding some minimal hooks in the core traceback machinery and let libraries like trio/asyncio handle the complicated parts?