Skip to content

Commit 0c797a6

Browse files
elprans1st1
authored andcommitted
bpo-34872: Fix self-cancellation in C implementation of asyncio.Task (GH-9679)
The C implementation of asyncio.Task currently fails to perform the cancellation cleanup correctly in the following scenario. async def task1(): async def task2(): await task3 # task3 is never cancelled asyncio.current_task().cancel() await asyncio.create_task(task2()) The actuall error is a hardcoded call to `future_cancel()` instead of calling the `cancel()` method of a future-like object. Thanks to Vladimir Matveev for noticing the code discrepancy and to Yury Selivanov for coming up with a pathological scenario.
1 parent 96c5932 commit 0c797a6

File tree

3 files changed

+45
-3
lines changed

3 files changed

+45
-3
lines changed

Lib/test/test_asyncio/test_tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,42 @@ def task():
622622
self.assertFalse(t._must_cancel) # White-box test.
623623
self.assertFalse(t.cancel())
624624

625+
def test_cancel_awaited_task(self):
626+
# This tests for a relatively rare condition when
627+
# a task cancellation is requested for a task which is not
628+
# currently blocked, such as a task cancelling itself.
629+
# In this situation we must ensure that whatever next future
630+
# or task the cancelled task blocks on is cancelled correctly
631+
# as well. See also bpo-34872.
632+
loop = asyncio.new_event_loop()
633+
self.addCleanup(lambda: loop.close())
634+
635+
task = nested_task = None
636+
fut = self.new_future(loop)
637+
638+
async def nested():
639+
await fut
640+
641+
async def coro():
642+
nonlocal nested_task
643+
# Create a sub-task and wait for it to run.
644+
nested_task = self.new_task(loop, nested())
645+
await asyncio.sleep(0)
646+
647+
# Request the current task to be cancelled.
648+
task.cancel()
649+
# Block on the nested task, which should be immediately
650+
# cancelled.
651+
await nested_task
652+
653+
task = self.new_task(loop, coro())
654+
with self.assertRaises(asyncio.CancelledError):
655+
loop.run_until_complete(task)
656+
657+
self.assertTrue(task.cancelled())
658+
self.assertTrue(nested_task.cancelled())
659+
self.assertTrue(fut.cancelled())
660+
625661
def test_stop_while_run_in_complete(self):
626662

627663
def gen():
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix self-cancellation in C implementation of asyncio.Task

Modules/_asynciomodule.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,14 +2713,19 @@ task_step_impl(TaskObj *task, PyObject *exc)
27132713

27142714
if (task->task_must_cancel) {
27152715
PyObject *r;
2716-
r = future_cancel(fut);
2716+
int is_true;
2717+
r = _PyObject_CallMethodId(fut, &PyId_cancel, NULL);
27172718
if (r == NULL) {
27182719
return NULL;
27192720
}
2720-
if (r == Py_True) {
2721+
is_true = PyObject_IsTrue(r);
2722+
Py_DECREF(r);
2723+
if (is_true < 0) {
2724+
return NULL;
2725+
}
2726+
else if (is_true) {
27212727
task->task_must_cancel = 0;
27222728
}
2723-
Py_DECREF(r);
27242729
}
27252730

27262731
Py_RETURN_NONE;

0 commit comments

Comments
 (0)