| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
The design follows EdgeDB monkey-patch. Speaking about public API, I can imagine an alternative design:
I believe the proposed design is cleaner; it doesn't allow .__cancel_requested__ setting outside of Task's implementation code. Resetting the flag before raising CancelledError allows handling cancellation ignorance by a task code. What do you think? |
Sorry, something went wrong.
There was a problem hiding this comment.
Thanks -- my concern here is that __cancel_requested__ is a rather unconventional API design. I would rather name it _cancel_requested, which is in line with the other attributes, or if we want this to be a truly public API (not just something for private use by asyncio.TaskGroup) make it a pair of methods, cancel_requested() returning a bool and set_cancel_requested() taking a bool.
Sorry, something went wrong.
|
@asvetlov I apologize, I didn't see your large comment with the alternative design before my own comment. This stuff is excruciating to understand (for me anyways, since I didn't write EdgeDb's TaskGroup :-) so I have to experiment some more. I will see if I can get the new TaskGroup to work with that, I will get back to you. |
Sorry, something went wrong.
|
@asvetlov I cannot get this to work. Here's what I did. (1) Implement Task.cancelling() as follows: diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index 2bee5c050d..6f726b2ff8 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -105,6 +105,7 @@ def __init__(self, coro, *, loop=None, name=None):
else:
self._name = str(name)
+ self._cancelling = False
self._must_cancel = False
self._fut_waiter = None
self._coro = coro
@@ -178,6 +179,9 @@ def print_stack(self, *, limit=None, file=None):
"""
return base_tasks._task_print_stack(self, limit, file)
+ def cancelling(self):
+ return self._cancelling
+
def cancel(self, msg=None):
"""Request that this task cancel itself.
@@ -201,6 +205,9 @@ def cancel(self, msg=None):
self._log_traceback = False
if self.done():
return False
+ if self._cancelling:
+ return False
+ self._cancelling = True
if self._fut_waiter is not None:
if self._fut_waiter.cancel(msg=msg):
# Leave self._fut_waiter; it may be a Task that
@@ -231,6 +238,7 @@ def __step(self, exc=None):
# don't have `__iter__` and `__next__` methods.
result = coro.send(None)
else:
+ self._cancelling = False
result = coro.throw(exc)
except StopIteration as exc:
if self._must_cancel:
(2) In taskgroups.py: Disable task patching in taskgroups.py; delete the assignment (of False) to __cancel_requested__; replace the check for __cancel_requested__ per your suggestion (sort of). diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py
index 4f946eb0e4..721115a2d8 100644
--- a/Lib/asyncio/taskgroups.py
+++ b/Lib/asyncio/taskgroups.py
@@ -78,7 +78,6 @@ async def __aenter__(self):
if self._parent_task is None:
raise RuntimeError(
f'TaskGroup {self!r} cannot determine the parent task')
- self._patch_task(self._parent_task)
return self
@@ -95,7 +94,7 @@ async def __aexit__(self, et, exc, tb):
if self._parent_cancel_requested:
# Only if we did request task to cancel ourselves
# we mark it as no longer cancelled.
- self._parent_task.__cancel_requested__ = False
+ pass
else:
propagate_cancellation_error = et
@@ -244,7 +243,7 @@ def _on_task_done(self, task):
return
self._abort()
- if not self._parent_task.__cancel_requested__:
+ if not self._parent_cancel_requested and not self._parent_task.cancelling():
# If parent task *is not* being cancelled, it means that we want
# to manually cancel it to abort whatever is being run right now
# in the TaskGroup. But we want to mark parent task as
(3) Disable the C accellerator at the top of test_taskgroups.py: diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 8a01985c06..2d909dad80 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -16,6 +16,9 @@ # limitations under the License. # +import sys +assert "_asyncio" not in sys.modules +sys.modules["_asyncio"] = None # Use the pure Python version import asyncio Now when I run ./python.exe -m test test_asyncio.test_taskgroups -v I get failures in test_taskgroup_{15,16}. In both cases I get an unexpected ExceptionGroup wrapping ZeroDivisionError, where a CancelledError is expected. Can you repro this? Can you explain or fix it? |
Sorry, something went wrong.
|
I think I understand the cause of the failure now (not sure this will lead to a fix yet). Test 15 has a try/except in the body of the async with TaskGroup() block, where in the try block it awaits a long sleep (which will be cancelled from the outside after 0.1 second), and the except catches CancelledError and then sleeps some more (0.5s) before re-raising it. During this second sleep a subtask crashes (0.3s into the test). The desired behavior here is that the sleep in the except block is not cancelled when the subtask crashes, and the sleep(0.5) is allowed to finish after which things reach __aexit__ with CancelledError. (Test 16 is a variation where there's another task wrapping the whole business -- the outermost task is cancelled, and this is expected to be propagated into the nested task.) Because the initial cancellation of the parent task happens from the outside, it is essential that its __cancel_requested__ flag remains set while the sleep(0.5) runs, so any flag that is reset when the coroutine is entered is not good enough, and a flag that is set only by the code in TaskGroup won't work either. We really need to remember that the parent task was cancelled across potentially many resumptions and suspensions of its coroutine. (Coincidentally this is also the thing that the Trio folks brought up in my original PR -- Yury solved it with this __cancel_requested__ behavior.) I did find that none of the tests fail if I replace the line self._parent_task.__cancel_requested__ = False with pass, so perhaps we do not actually need the ability to reset this flag; or maybe there's a missing test. However, the check if self._parent_cancel_requested is required; if I just always set propagate_cancellation_error = et once we get to that point, some tests fail. That's as far as I got. If we're going to design a public API for this, it would have to be a flag on tasks that is set when the task is cancelled, and which then stays set for the duration. We could add an API to reset the flag, which could be used by a task that catches cancellation and decides to ignore it (rather than just taking its time to clean up, like tests 15 and 16). Until the flag is cleared, further efforts to cancel the task are ignored. |
Sorry, something went wrong.
|
@asvetlov I would like to make progress on this. Mind if I just incorporate my version into the TaskGroup PR? My proposal is the following:
(@agronholm I think this may solve your problem too.) |
Sorry, something went wrong.
This means we no longer have to monkey-patch the parent task. It does introduce new semantics for task cancellation: When a task is cancelled, further attempts to cancel it have *no* effect unless the task calls self.uncancel(). Borrowed from pythonGH-31313 by @asvetlov.
|
Thanks, @gvanrossum |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Add __cancel_requested__ property as discussed in #31270.
cancel() returns False is the task was previously cancelled already.
No test is failing during the cancel() behavior change.