Skip to content

Commit 7fce106

Browse files
authored
bpo-46771: Implement task cancel requests counter (GH-31513)
This changes cancelling() and uncancel() to return the count of pending cancellations. This can be used to avoid bugs in certain edge cases (e.g. two timeouts going off at the same time).
1 parent 6e117e7 commit 7fce106

File tree

3 files changed

+48
-46
lines changed

3 files changed

+48
-46
lines changed

Lib/asyncio/tasks.py

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

108-
self._cancel_requested = False
108+
self._num_cancels_requested = 0
109109
self._must_cancel = False
110110
self._fut_waiter = None
111111
self._coro = coro
@@ -198,13 +198,15 @@ def cancel(self, msg=None):
198198
task will be marked as cancelled when the wrapped coroutine
199199
terminates with a CancelledError exception (even if cancel()
200200
was not called).
201+
202+
This also increases the task's count of cancellation requests.
201203
"""
202204
self._log_traceback = False
203205
if self.done():
204206
return False
205-
if self._cancel_requested:
207+
self._num_cancels_requested += 1
208+
if self._num_cancels_requested > 1:
206209
return False
207-
self._cancel_requested = True
208210
if self._fut_waiter is not None:
209211
if self._fut_waiter.cancel(msg=msg):
210212
# Leave self._fut_waiter; it may be a Task that
@@ -217,14 +219,24 @@ def cancel(self, msg=None):
217219
return True
218220

219221
def cancelling(self):
220-
return self._cancel_requested
222+
"""Return the count of the task's cancellation requests.
223+
224+
This count is incremented when .cancel() is called
225+
and may be decremented using .uncancel().
226+
"""
227+
return self._num_cancels_requested
221228

222229
def uncancel(self):
223-
if self._cancel_requested:
224-
self._cancel_requested = False
225-
return True
226-
else:
227-
return False
230+
"""Decrement the task's count of cancellation requests.
231+
232+
This should be used by tasks that catch CancelledError
233+
and wish to continue indefinitely until they are cancelled again.
234+
235+
Returns the remaining number of cancellation requests.
236+
"""
237+
if self._num_cancels_requested > 0:
238+
self._num_cancels_requested -= 1
239+
return self._num_cancels_requested
228240

229241
def __step(self, exc=None):
230242
if self.done():

Modules/_asynciomodule.c

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ typedef struct {
9191
PyObject *task_context;
9292
int task_must_cancel;
9393
int task_log_destroy_pending;
94-
int task_cancel_requested;
94+
int task_num_cancels_requested;
9595
} TaskObj;
9696

9797
typedef struct {
@@ -2036,7 +2036,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
20362036
Py_CLEAR(self->task_fut_waiter);
20372037
self->task_must_cancel = 0;
20382038
self->task_log_destroy_pending = 1;
2039-
self->task_cancel_requested = 0;
2039+
self->task_num_cancels_requested = 0;
20402040
Py_INCREF(coro);
20412041
Py_XSETREF(self->task_coro, coro);
20422042

@@ -2191,22 +2191,24 @@ not return True (unless the task was already cancelled). A
21912191
task will be marked as cancelled when the wrapped coroutine
21922192
terminates with a CancelledError exception (even if cancel()
21932193
was not called).
2194+
2195+
This also increases the task's count of cancellation requests.
21942196
[clinic start generated code]*/
21952197

21962198
static PyObject *
21972199
_asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
2198-
/*[clinic end generated code: output=c66b60d41c74f9f1 input=f4ff8e8ffc5f1c00]*/
2200+
/*[clinic end generated code: output=c66b60d41c74f9f1 input=7bb51bf25974c783]*/
21992201
{
22002202
self->task_log_tb = 0;
22012203

22022204
if (self->task_state != STATE_PENDING) {
22032205
Py_RETURN_FALSE;
22042206
}
22052207

2206-
if (self->task_cancel_requested) {
2208+
self->task_num_cancels_requested += 1;
2209+
if (self->task_num_cancels_requested > 1) {
22072210
Py_RETURN_FALSE;
22082211
}
2209-
self->task_cancel_requested = 1;
22102212

22112213
if (self->task_fut_waiter) {
22122214
PyObject *res;
@@ -2238,51 +2240,40 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
22382240
/*[clinic input]
22392241
_asyncio.Task.cancelling
22402242
2241-
Return True if the task is in the process of being cancelled.
2242-
2243-
This is set once .cancel() is called
2244-
and remains set until .uncancel() is called.
2243+
Return the count of the task's cancellation requests.
22452244
2246-
As long as this flag is set, further .cancel() calls will be ignored,
2247-
until .uncancel() is called to reset it.
2245+
This count is incremented when .cancel() is called
2246+
and may be decremented using .uncancel().
22482247
[clinic start generated code]*/
22492248

22502249
static PyObject *
22512250
_asyncio_Task_cancelling_impl(TaskObj *self)
2252-
/*[clinic end generated code: output=803b3af96f917d7e input=c50e50f9c3ca4676]*/
2251+
/*[clinic end generated code: output=803b3af96f917d7e input=b625224d310cbb17]*/
22532252
/*[clinic end generated code]*/
22542253
{
2255-
if (self->task_cancel_requested) {
2256-
Py_RETURN_TRUE;
2257-
}
2258-
else {
2259-
Py_RETURN_FALSE;
2260-
}
2254+
return PyLong_FromLong(self->task_num_cancels_requested);
22612255
}
22622256

22632257
/*[clinic input]
22642258
_asyncio.Task.uncancel
22652259
2266-
Reset the flag returned by cancelling().
2260+
Decrement the task's count of cancellation requests.
22672261
22682262
This should be used by tasks that catch CancelledError
22692263
and wish to continue indefinitely until they are cancelled again.
22702264
2271-
Returns the previous value of the flag.
2265+
Returns the remaining number of cancellation requests.
22722266
[clinic start generated code]*/
22732267

22742268
static PyObject *
22752269
_asyncio_Task_uncancel_impl(TaskObj *self)
2276-
/*[clinic end generated code: output=58184d236a817d3c input=5db95e28fcb6f7cd]*/
2270+
/*[clinic end generated code: output=58184d236a817d3c input=68f81a4b90b46be2]*/
22772271
/*[clinic end generated code]*/
22782272
{
2279-
if (self->task_cancel_requested) {
2280-
self->task_cancel_requested = 0;
2281-
Py_RETURN_TRUE;
2282-
}
2283-
else {
2284-
Py_RETURN_FALSE;
2273+
if (self->task_num_cancels_requested > 0) {
2274+
self->task_num_cancels_requested -= 1;
22852275
}
2276+
return PyLong_FromLong(self->task_num_cancels_requested);
22862277
}
22872278

22882279
/*[clinic input]

Modules/clinic/_asynciomodule.c.h

Lines changed: 9 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)