Skip to content

Commit a67bd53

Browse files
elpransmiss-islington
authored andcommitted
[3.7] bpo-34872: Fix self-cancellation in C implementation of asyncio.Task (GH-9679) (GH-9691)
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) Co-authored-by: Elvis Pranskevichus <elvis@magic.io> https://bugs.python.org/issue34872
1 parent 063755c commit a67bd53

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
@@ -590,6 +590,42 @@ def task():
590590
self.assertFalse(t._must_cancel) # White-box test.
591591
self.assertFalse(t.cancel())
592592

593+
def test_cancel_awaited_task(self):
594+
# This tests for a relatively rare condition when
595+
# a task cancellation is requested for a task which is not
596+
# currently blocked, such as a task cancelling itself.
597+
# In this situation we must ensure that whatever next future
598+
# or task the cancelled task blocks on is cancelled correctly
599+
# as well. See also bpo-34872.
600+
loop = asyncio.new_event_loop()
601+
self.addCleanup(lambda: loop.close())
602+
603+
task = nested_task = None
604+
fut = self.new_future(loop)
605+
606+
async def nested():
607+
await fut
608+
609+
async def coro():
610+
nonlocal nested_task
611+
# Create a sub-task and wait for it to run.
612+
nested_task = self.new_task(loop, nested())
613+
await asyncio.sleep(0)
614+
615+
# Request the current task to be cancelled.
616+
task.cancel()
617+
# Block on the nested task, which should be immediately
618+
# cancelled.
619+
await nested_task
620+
621+
task = self.new_task(loop, coro())
622+
with self.assertRaises(asyncio.CancelledError):
623+
loop.run_until_complete(task)
624+
625+
self.assertTrue(task.cancelled())
626+
self.assertTrue(nested_task.cancelled())
627+
self.assertTrue(fut.cancelled())
628+
593629
def test_stop_while_run_in_complete(self):
594630

595631
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
@@ -2662,14 +2662,19 @@ task_step_impl(TaskObj *task, PyObject *exc)
26622662

26632663
if (task->task_must_cancel) {
26642664
PyObject *r;
2665-
r = future_cancel(fut);
2665+
int is_true;
2666+
r = _PyObject_CallMethodId(fut, &PyId_cancel, NULL);
26662667
if (r == NULL) {
26672668
return NULL;
26682669
}
2669-
if (r == Py_True) {
2670+
is_true = PyObject_IsTrue(r);
2671+
Py_DECREF(r);
2672+
if (is_true < 0) {
2673+
return NULL;
2674+
}
2675+
else if (is_true) {
26702676
task->task_must_cancel = 0;
26712677
}
2672-
Py_DECREF(r);
26732678
}
26742679

26752680
Py_RETURN_NONE;

0 commit comments

Comments
 (0)