Skip to content

Commit 77ec0e4

Browse files
committed
Add an API to Task to manage 'cancel_requested' flag
This means we no longer have to monkey-patch the parent task. It does introduce new semantics for task cancellation: When a task is cancelled, further attempts to cancel it have *no* effect unless the task calls self.uncancel(). Borrowed from pythonGH-31313 by @asvetlov.
1 parent 0e1355d commit 77ec0e4

4 files changed

Lines changed: 124 additions & 25 deletions

File tree

Lib/asyncio/taskgroups.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ async def __aenter__(self):
7878
if self._parent_task is None:
7979
raise RuntimeError(
8080
f'TaskGroup {self!r} cannot determine the parent task')
81-
self._patch_task(self._parent_task)
8281

8382
return self
8483

@@ -95,7 +94,7 @@ async def __aexit__(self, et, exc, tb):
9594
if self._parent_cancel_requested:
9695
# Only if we did request task to cancel ourselves
9796
# we mark it as no longer cancelled.
98-
self._parent_task.__cancel_requested__ = False
97+
self._parent_task.uncancel()
9998
else:
10099
propagate_cancellation_error = et
101100

@@ -186,26 +185,6 @@ def _is_base_error(self, exc: BaseException) -> bool:
186185
assert isinstance(exc, BaseException)
187186
return isinstance(exc, (SystemExit, KeyboardInterrupt))
188187

189-
def _patch_task(self, task):
190-
# In the future we'll need proper API on asyncio.Task to
191-
# make TaskGroups possible. We need to be able to access
192-
# information about task cancellation, more specifically,
193-
# we need a flag to say if a task was cancelled or not.
194-
# We also need to be able to flip that flag.
195-
196-
def _task_cancel(self, msg=None):
197-
self.__cancel_requested__ = True
198-
return asyncio.Task.cancel(self, msg)
199-
200-
if hasattr(task, '__cancel_requested__'):
201-
return
202-
203-
task.__cancel_requested__ = False
204-
# confirm that we were successful at adding the new attribute:
205-
assert not task.__cancel_requested__
206-
207-
task.cancel = types.MethodType(_task_cancel, task)
208-
209188
def _abort(self):
210189
self._aborting = True
211190

@@ -244,7 +223,7 @@ def _on_task_done(self, task):
244223
return
245224

246225
self._abort()
247-
if not self._parent_task.__cancel_requested__:
226+
if not self._parent_task.cancelling():
248227
# If parent task *is not* being cancelled, it means that we want
249228
# to manually cancel it to abort whatever is being run right now
250229
# in the TaskGroup. But we want to mark parent task as

Lib/asyncio/tasks.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def __init__(self, coro, *, loop=None, name=None):
105105
else:
106106
self._name = str(name)
107107

108+
self._cancel_requested = False
108109
self._must_cancel = False
109110
self._fut_waiter = None
110111
self._coro = coro
@@ -201,6 +202,9 @@ def cancel(self, msg=None):
201202
self._log_traceback = False
202203
if self.done():
203204
return False
205+
if self._cancel_requested:
206+
return False
207+
self._cancel_requested = True
204208
if self._fut_waiter is not None:
205209
if self._fut_waiter.cancel(msg=msg):
206210
# Leave self._fut_waiter; it may be a Task that
@@ -212,6 +216,16 @@ def cancel(self, msg=None):
212216
self._cancel_message = msg
213217
return True
214218

219+
def cancelling(self):
220+
return self._cancel_requested
221+
222+
def uncancel(self):
223+
if self._cancel_requested:
224+
self._cancel_requested = False
225+
return True
226+
else:
227+
return False
228+
215229
def __step(self, exc=None):
216230
if self.done():
217231
raise exceptions.InvalidStateError(
@@ -634,7 +648,7 @@ def _ensure_future(coro_or_future, *, loop=None):
634648
loop = events._get_event_loop(stacklevel=4)
635649
try:
636650
return loop.create_task(coro_or_future)
637-
except RuntimeError:
651+
except RuntimeError:
638652
if not called_wrap_awaitable:
639653
coro_or_future.close()
640654
raise

Modules/_asynciomodule.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ typedef struct {
9090
PyObject *task_context;
9191
int task_must_cancel;
9292
int task_log_destroy_pending;
93+
int task_cancel_requested;
9394
} TaskObj;
9495

9596
typedef struct {
@@ -2038,6 +2039,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
20382039
Py_CLEAR(self->task_fut_waiter);
20392040
self->task_must_cancel = 0;
20402041
self->task_log_destroy_pending = 1;
2042+
self->task_cancel_requested = 0;
20412043
Py_INCREF(coro);
20422044
Py_XSETREF(self->task_coro, coro);
20432045

@@ -2204,6 +2206,11 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
22042206
Py_RETURN_FALSE;
22052207
}
22062208

2209+
if (self->task_cancel_requested) {
2210+
Py_RETURN_FALSE;
2211+
}
2212+
self->task_cancel_requested = 1;
2213+
22072214
if (self->task_fut_waiter) {
22082215
PyObject *res;
22092216
int is_true;
@@ -2231,6 +2238,56 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
22312238
Py_RETURN_TRUE;
22322239
}
22332240

2241+
/*[clinic input]
2242+
_asyncio.Task.cancelling
2243+
2244+
Return True if the task is in the process of being cancelled.
2245+
2246+
This is set once .cancel() is called
2247+
and remains set until .uncancel() is called.
2248+
2249+
As long as this flag is set, further .cancel() calls will be ignored,
2250+
until .uncancel() is called to reset it.
2251+
[clinic start generated code]*/
2252+
2253+
static PyObject *
2254+
_asyncio_Task_cancelling_impl(TaskObj *self)
2255+
/*[clinic end generated code: output=803b3af96f917d7e input=c50e50f9c3ca4676]*/
2256+
/*[clinic end generated code]*/
2257+
{
2258+
if (self->task_cancel_requested) {
2259+
Py_RETURN_TRUE;
2260+
}
2261+
else {
2262+
Py_RETURN_FALSE;
2263+
}
2264+
}
2265+
2266+
/*[clinic input]
2267+
_asyncio.Task.uncancel
2268+
2269+
Reset the flag returned by cancelling().
2270+
2271+
This should be used by tasks that catch CancelledError
2272+
and wish to continue indefinitely until they are cancelled again.
2273+
2274+
Returns the previous value of the flag.
2275+
[clinic start generated code]*/
2276+
2277+
static PyObject *
2278+
_asyncio_Task_uncancel_impl(TaskObj *self)
2279+
/*[clinic end generated code: output=58184d236a817d3c input=5db95e28fcb6f7cd]*/
2280+
/*[clinic end generated code]*/
2281+
{
2282+
if (self->task_cancel_requested) {
2283+
self->task_cancel_requested = 0;
2284+
Py_RETURN_TRUE;
2285+
}
2286+
else {
2287+
Py_RETURN_FALSE;
2288+
}
2289+
}
2290+
22342291
/*[clinic input]
22352292
_asyncio.Task.get_stack
22362293
@@ -2454,6 +2511,8 @@ static PyMethodDef TaskType_methods[] = {
24542511
_ASYNCIO_TASK_SET_RESULT_METHODDEF
24552512
_ASYNCIO_TASK_SET_EXCEPTION_METHODDEF
24562513
_ASYNCIO_TASK_CANCEL_METHODDEF
2514+
_ASYNCIO_TASK_CANCELLING_METHODDEF
2515+
_ASYNCIO_TASK_UNCANCEL_METHODDEF
24572516
_ASYNCIO_TASK_GET_STACK_METHODDEF
24582517
_ASYNCIO_TASK_PRINT_STACK_METHODDEF
24592518
_ASYNCIO_TASK__MAKE_CANCELLED_ERROR_METHODDEF

Modules/clinic/_asynciomodule.c.h

Lines changed: 48 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)