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.
This commit is contained in:
Elvis Pranskevichus 2018-10-03 10:30:31 -04:00 committed by Yury Selivanov
parent 96c5932794
commit 0c797a6aca
3 changed files with 45 additions and 3 deletions

View File

@ -622,6 +622,42 @@ class BaseTaskTests:
self.assertFalse(t._must_cancel) # White-box test. self.assertFalse(t._must_cancel) # White-box test.
self.assertFalse(t.cancel()) self.assertFalse(t.cancel())
def test_cancel_awaited_task(self):
# This tests for a relatively rare condition when
# a task cancellation is requested for a task which is not
# currently blocked, such as a task cancelling itself.
# In this situation we must ensure that whatever next future
# or task the cancelled task blocks on is cancelled correctly
# as well. See also bpo-34872.
loop = asyncio.new_event_loop()
self.addCleanup(lambda: loop.close())
task = nested_task = None
fut = self.new_future(loop)
async def nested():
await fut
async def coro():
nonlocal nested_task
# Create a sub-task and wait for it to run.
nested_task = self.new_task(loop, nested())
await asyncio.sleep(0)
# Request the current task to be cancelled.
task.cancel()
# Block on the nested task, which should be immediately
# cancelled.
await nested_task
task = self.new_task(loop, coro())
with self.assertRaises(asyncio.CancelledError):
loop.run_until_complete(task)
self.assertTrue(task.cancelled())
self.assertTrue(nested_task.cancelled())
self.assertTrue(fut.cancelled())
def test_stop_while_run_in_complete(self): def test_stop_while_run_in_complete(self):
def gen(): def gen():

View File

@ -0,0 +1 @@
Fix self-cancellation in C implementation of asyncio.Task

View File

@ -2713,14 +2713,19 @@ set_exception:
if (task->task_must_cancel) { if (task->task_must_cancel) {
PyObject *r; PyObject *r;
r = future_cancel(fut); int is_true;
r = _PyObject_CallMethodId(fut, &PyId_cancel, NULL);
if (r == NULL) { if (r == NULL) {
return NULL; return NULL;
} }
if (r == Py_True) { is_true = PyObject_IsTrue(r);
Py_DECREF(r);
if (is_true < 0) {
return NULL;
}
else if (is_true) {
task->task_must_cancel = 0; task->task_must_cancel = 0;
} }
Py_DECREF(r);
} }
Py_RETURN_NONE; Py_RETURN_NONE;