Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1517,3 +1517,12 @@ Task object
used by end-user code. See :meth:`uncancel` for more details.

.. versionadded:: 3.11

.. method:: cancelling_since()

Return the event loop time at which :meth:`cancel` was first called, or
``None`` if cancellation has not been requested.

Repeated calls to :meth:`cancel` do not change the recorded time.

.. versionadded:: 3.16
6 changes: 6 additions & 0 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def __init__(self, coro, *, loop=None, name=None, context=None,
self._name = str(name)

self._num_cancels_requested = 0
self._cancel_requested_at = None
self._must_cancel = False
self._fut_waiter = None
self._coro = coro
Expand Down Expand Up @@ -206,6 +207,8 @@ def cancel(self, msg=None):
if self.done():
return False
self._num_cancels_requested += 1
if self._num_cancels_requested == 1:
self._cancel_requested_at = self._loop.time()
# These two lines are controversial. See discussion starting at
# https://github.com/python/cpython/pull/31394#issuecomment-1053545331
# Also remember that this is duplicated in _asynciomodule.c.
Expand All @@ -230,6 +233,9 @@ def cancelling(self):
"""
return self._num_cancels_requested

def cancelling_since(self):
return self._cancel_requested_at

def uncancel(self):
"""Decrement the task's count of cancellation requests.

Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,56 @@ async def task():
finally:
loop.close()

def test_cancelling_since_none_before_cancel(self):
loop = asyncio.new_event_loop()

async def task():
await asyncio.sleep(10)

try:
t = self.new_task(loop, task())
self.assertIsNone(t.cancelling_since())
t.cancel()
with self.assertRaises(asyncio.CancelledError):
loop.run_until_complete(t)
finally:
loop.close()

def test_cancelling_since_is_float_after_cancel(self):
loop = asyncio.new_event_loop()

async def task():
await asyncio.sleep(10)

try:
t = self.new_task(loop, task())
t.cancel()
ts = t.cancelling_since()
self.assertIsInstance(ts, float)
self.assertGreater(ts, 0)
with self.assertRaises(asyncio.CancelledError):
loop.run_until_complete(t)
finally:
loop.close()

def test_cancelling_since_stable_across_multiple_cancels(self):
loop = asyncio.new_event_loop()

async def task():
await asyncio.sleep(10)

try:
t = self.new_task(loop, task())
t.cancel()
first = t.cancelling_since()
t.cancel()
t.cancel()
self.assertEqual(t.cancelling_since(), first)
with self.assertRaises(asyncio.CancelledError):
loop.run_until_complete(t)
finally:
loop.close()

def test_uncancel_basic(self):
loop = asyncio.new_event_loop()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`asyncio.Task.cancelling_since` to report when cancellation was
first requested for a task.
28 changes: 28 additions & 0 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ typedef struct TaskObj {
unsigned task_must_cancel: 1;
unsigned task_log_destroy_pending: 1;
int task_num_cancels_requested;
PyObject *task_cancel_requested_at; /* loop time of first cancel(), or Py_None */
PyObject *task_fut_waiter;
PyObject *task_coro;
PyObject *task_name;
Expand Down Expand Up @@ -2342,6 +2343,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
self->task_must_cancel = 0;
self->task_log_destroy_pending = 1;
self->task_num_cancels_requested = 0;
self->task_cancel_requested_at = Py_NewRef(Py_None);
set_task_coro(self, coro);

if (name == Py_None) {
Expand Down Expand Up @@ -2399,6 +2401,7 @@ TaskObj_clear(PyObject *op)
clear_task_coro(task);
Py_CLEAR(task->task_context);
Py_CLEAR(task->task_name);
Py_CLEAR(task->task_cancel_requested_at);
Py_CLEAR(task->task_fut_waiter);
return 0;
}
Expand All @@ -2411,6 +2414,7 @@ TaskObj_traverse(PyObject *op, visitproc visit, void *arg)
Py_VISIT(task->task_context);
Py_VISIT(task->task_coro);
Py_VISIT(task->task_name);
Py_VISIT(task->task_cancel_requested_at);
Py_VISIT(task->task_fut_waiter);
FutureObj *fut = (FutureObj *)task;
Py_VISIT(fut->fut_loop);
Expand Down Expand Up @@ -2588,6 +2592,15 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)

self->task_num_cancels_requested += 1;

if (self->task_num_cancels_requested == 1) {
PyObject *t = PyObject_CallMethodNoArgs(
((FutureObj *)self)->fut_loop, &_Py_ID(time));
if (t == NULL) {
return NULL;
}
Py_SETREF(self->task_cancel_requested_at, t);
}

// These three lines are controversial. See discussion starting at
// https://github.com/python/cpython/pull/31394#issuecomment-1053545331
// and corresponding code in tasks.py.
Expand Down Expand Up @@ -2640,6 +2653,20 @@ _asyncio_Task_cancelling_impl(TaskObj *self)
return PyLong_FromLong(self->task_num_cancels_requested);
}

/*[clinic input]
@critical_section
_asyncio.Task.cancelling_since

Return the loop time when cancel() was first called, or None.
[clinic start generated code]*/

static PyObject *
_asyncio_Task_cancelling_since_impl(TaskObj *self)
/*[clinic end generated code: output=2670bb55581ab10e input=1f81f0a3b6e3da63]*/
{
return Py_NewRef(self->task_cancel_requested_at);
}

/*[clinic input]
@critical_section
_asyncio.Task.uncancel
Expand Down Expand Up @@ -2918,6 +2945,7 @@ static PyMethodDef TaskType_methods[] = {
_ASYNCIO_TASK_SET_EXCEPTION_METHODDEF
_ASYNCIO_TASK_CANCEL_METHODDEF
_ASYNCIO_TASK_CANCELLING_METHODDEF
_ASYNCIO_TASK_CANCELLING_SINCE_METHODDEF
_ASYNCIO_TASK_UNCANCEL_METHODDEF
_ASYNCIO_TASK_GET_STACK_METHODDEF
_ASYNCIO_TASK_PRINT_STACK_METHODDEF
Expand Down
26 changes: 25 additions & 1 deletion Modules/clinic/_asynciomodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading