Skip to content

Commit 166773d

Browse files
elpransmiss-islington
authored andcommitted
[3.6] bpo-34872: Fix self-cancellation in C implementation of asyncio.Task (GH-9679) (GH-9690)
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. (cherry picked from commit 548ce9d) https://bugs.python.org/issue34872
1 parent 6580e52 commit 166773d

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

Lib/test/test_asyncio/test_tasks.py

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

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

626662
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
@@ -2121,14 +2121,19 @@ task_step_impl(TaskObj *task, PyObject *exc)
21212121

21222122
if (task->task_must_cancel) {
21232123
PyObject *r;
2124-
r = future_cancel(fut);
2124+
int is_true;
2125+
r = _PyObject_CallMethodId(fut, &PyId_cancel, NULL);
21252126
if (r == NULL) {
21262127
return NULL;
21272128
}
2128-
if (r == Py_True) {
2129+
is_true = PyObject_IsTrue(r);
2130+
Py_DECREF(r);
2131+
if (is_true < 0) {
2132+
return NULL;
2133+
}
2134+
else if (is_true) {
21292135
task->task_must_cancel = 0;
21302136
}
2131-
Py_DECREF(r);
21322137
}
21332138

21342139
Py_RETURN_NONE;

0 commit comments

Comments
 (0)