-
Notifications
You must be signed in to change notification settings - Fork 145
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
Updated cancellation semantics to better match the new trio and asyncio ones #586
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.
This looks great! I have some minor comments.
Does this also address cases from #432?
src/anyio/_backends/_asyncio.py
Outdated
elif exc_val is not None and not ignore_exception: | ||
raise exc_val |
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 is the follow-up for "reraise the CancelledError later unless this task was already interrupted by another exception" right? As well as if exc_val
is not null on context exit? Perhaps a comment about these cases (particularly when the second case would occur) would be helpful.
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 was just about the hardest part to figure out, as AnyIO is treading into uncharted territory as far as cancellation semantics go. I need to double check that I got this right, or at least that I've made it behave consistently.
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.
IIRC the idea was to raise a single cancellation exception if the host task was cancelled and there were no other errors.
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.
At least asyncio.TaskGroup
behaves this way.
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.
In the case where exc_val
is the exception originally pased to __aexit__
, you will get a nicer traceback (no extraneous frame) if you return False
rather than explicitly reraising it.
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.
Sure it does -- save the original exc_val
and compare its identity against the current one.
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.
I didn't mean this wasn't fixable. What I left unsaid was that I would need to add an extra local variable just for distinguishing between these two cases.
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.
It might be clearer not to reassign exc_val
anyway. That'd make the case I had questions about at #586 (comment) more obvious.
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.
Maybe you're right. I'll poke the code and see what I come up with.
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.
I opted not to store any CancellationErrors which then simplifies the code somewhat. I just pushed an update.
Co-authored-by: Zanie Blue <contact@zanie.dev>
Co-authored-by: Zanie Blue <contact@zanie.dev>
``ExceptionGroup`` (or ``BaseExceptionGroup`` if one or more ``BaseException`` were | ||
included) | ||
- Fixed task group not raising a cancellation exception on asyncio at exit if no child | ||
tasks were spawned and an outer cancellation scope had been cancelled before |
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.
Trio would say that a Cancelled exception means some work was interrupted. In this case, it sounds like no work was interrupted, so I'm not sure it's actually important to raise the exception. Trio does raise the exception, because of its rule that every async operation that can block (including NurseryManager.__aexit__
) is always both a schedule point and a cancel point, but this has caused problems occasionally and there has been recent discussion of changing it; IIRC consensus was that it would be fine to change but no one has done it yet. Discussion in python-trio/trio#1457.
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 issue that this refers to was that an AnyIO TaskGroup
would swallow a CancelledError
that was meant to cancel the entire task.
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.
Edge-triggered cancellation rears its ugly head again!
My intuition is that for fully consistent behavior here you need to treat each asyncio task as implicitly enclosed in a CancelScope that would be cancelled by task.cancel()
, and include that implicit cancel scope in your logic of whether each cancel scope should swallow a cancellation or forward it outward. You can indirectly detect whether a task-level cancellation has occurred, at least on 3.11+, by counting anyio-originated calls to task.cancel()
that are not yet uncancelled, and comparing that against task.cancelling()
.
Making task group exit a cancel point is defensible though. Even if Trio stops doing that, you can easily add it back by putting in an explicit checkpoint_if_cancelled
.
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.
treat each asyncio task as implicitly enclosed in a CancelScope that would be cancelled by task.cancel().
This is actually something I was going to propose for asyncio! I'll be sending that post to Async-SIG shortly. Meanwhile, I can again look for a workaround for Python < 3.11, but that should not block this PR.
The differences in what comes out when cancellation happens is not really something I can reasonably control. Trio does its own thing and asyncio needs a single |
So, what's the verdict? Are more changes needed here? |
if not waited_for_tasks_to_finish: | ||
await AsyncIOBackend.checkpoint() |
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.
Worth documenting why we checkpoint 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.
Clarified.
src/anyio/_backends/_asyncio.py
Outdated
# Raise the CancelledError received while waiting for child tasks to exit, | ||
# unless the context manager itself was previously exited with another exception |
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 will also be skipped if an exception was raised in the the group (i.e. a value is in self._exceptions
) from the clause above. Perhaps worth noting for clarity.
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.
Clarified.
if cancelled_exc_while_waiting_tasks: | ||
if exc_val is None or ignore_exception: |
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.
Nit: This could be one statement
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.
It would not increase readability. I've seen something similar used in CPython code, and my initial thoughts were the same as yours until I saw the wisdom in doing it like this.
Overall I think this is clearer than before! I poked at it a little bit but couldn't come up with a better way to express the logic. |
I'm fairly happy with this, so I'm merging. If anything comes up, we can sort it out during the RC cycle. |
So I just talked to Joshua Oreman and it seems that Trio wouldn't have an issue with a single |
This is a lighter version of #496 which I could never get right.
Fixes #374.