Skip to content

Commit 17e4c9e

Browse files
committed
Implement num_cancels_requested
1 parent 9391940 commit 17e4c9e

File tree

3 files changed

+91
-27
lines changed

3 files changed

+91
-27
lines changed

Lib/asyncio/tasks.py

Lines changed: 9 additions & 11 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
@@ -202,9 +202,9 @@ def cancel(self, msg=None):
202202
self._log_traceback = False
203203
if self.done():
204204
return False
205-
if self._cancel_requested:
205+
self._num_cancels_requested += 1
206+
if self._num_cancels_requested > 1:
206207
return False
207-
self._cancel_requested = True
208208
if self._fut_waiter is not None:
209209
if self._fut_waiter.cancel(msg=msg):
210210
# Leave self._fut_waiter; it may be a Task that
@@ -216,15 +216,13 @@ def cancel(self, msg=None):
216216
self._cancel_message = msg
217217
return True
218218

219-
def cancelling(self):
220-
return self._cancel_requested
219+
def cancelling(self) -> int:
220+
return self._num_cancels_requested
221221

222-
def uncancel(self):
223-
if self._cancel_requested:
224-
self._cancel_requested = False
225-
return True
226-
else:
227-
return False
222+
def uncancel(self) -> int:
223+
if self._num_cancels_requested > 0:
224+
self._num_cancels_requested -= 1
225+
return self._num_cancels_requested
228226

229227
def __step(self, exc=None):
230228
if self.done():

Lib/test/test_asyncio/test_timeouts.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for asyncio/timeouts.py"""
22

33
import unittest
4+
import time
45

56
import asyncio
67
from asyncio import tasks
@@ -16,6 +17,31 @@ class BaseTimeoutTests:
1617
def new_task(self, loop, coro, name='TestTask'):
1718
return self.__class__.Task(coro, loop=loop, name=name)
1819

20+
def _setupAsyncioLoop(self):
21+
assert self._asyncioTestLoop is None, 'asyncio test loop already initialized'
22+
loop = asyncio.new_event_loop()
23+
asyncio.set_event_loop(loop)
24+
loop.set_debug(True)
25+
self._asyncioTestLoop = loop
26+
loop.set_task_factory(self.new_task)
27+
fut = loop.create_future()
28+
self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut))
29+
loop.run_until_complete(fut)
30+
31+
async def test_cancel_scope_basic(self):
32+
with asyncio.cancel_scope(0.1) as scope:
33+
await asyncio.sleep(10)
34+
self.assertTrue(scope.cancelling())
35+
self.assertTrue(scope.cancelled)
36+
37+
async def test_cancel_scope_at_basic(self):
38+
loop = asyncio.get_running_loop()
39+
40+
with asyncio.cancel_scope_at(loop.time() + 0.1) as scope:
41+
await asyncio.sleep(10)
42+
self.assertTrue(scope.cancelling())
43+
self.assertTrue(scope.cancelled)
44+
1945
async def test_timeout_basic(self):
2046
with self.assertRaises(TimeoutError):
2147
async with asyncio.timeout(0.01) as cm:
@@ -137,6 +163,54 @@ async def outer() -> None:
137163
assert not task.cancelled()
138164
assert task.done()
139165

166+
async def test_nested_timeouts(self):
167+
with self.assertRaises(TimeoutError):
168+
with asyncio.timeout(0.1) as outer:
169+
try:
170+
with asyncio.timeout(0.2) as inner:
171+
await asyncio.sleep(10)
172+
except asyncio.TimeoutError:
173+
# Pretend we start a super long operation here.
174+
self.assertTrue(False)
175+
176+
self.assertTrue(outer.cancelled)
177+
178+
async def test_nested_timeouts_concurrent(self):
179+
with self.assertRaises(TimeoutError):
180+
with asyncio.timeout(0.002):
181+
try:
182+
with asyncio.timeout(0.003):
183+
# Pretend we crunch some numbers.
184+
time.sleep(0.005)
185+
await asyncio.sleep(1)
186+
except asyncio.TimeoutError:
187+
pass
188+
189+
async def test_nested_timeouts_loop_busy(self):
190+
"""
191+
After the inner timeout is an expensive operation which should
192+
be stopped by the outer timeout.
193+
194+
Note: this fails for now.
195+
"""
196+
start = time.perf_counter()
197+
try:
198+
with asyncio.timeout(0.002) as outer:
199+
try:
200+
with asyncio.timeout(0.001) as inner:
201+
# Pretend the loop is busy for a while.
202+
time.sleep(0.010)
203+
await asyncio.sleep(0.001)
204+
except asyncio.TimeoutError:
205+
# This sleep should be interrupted.
206+
await asyncio.sleep(0.050)
207+
except asyncio.TimeoutError:
208+
pass
209+
took = time.perf_counter() - start
210+
self.assertTrue(took <= 0.015)
211+
self.assertTrue(outer.cancelled)
212+
self.assertTrue(inner.cancelled)
213+
140214

141215
@unittest.skipUnless(hasattr(tasks, '_CTask'),
142216
'requires the C _asyncio module')

Modules/_asynciomodule.c

Lines changed: 8 additions & 16 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 {
@@ -2040,7 +2040,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
20402040
Py_CLEAR(self->task_fut_waiter);
20412041
self->task_must_cancel = 0;
20422042
self->task_log_destroy_pending = 1;
2043-
self->task_cancel_requested = 0;
2043+
self->task_num_cancels_requested = 0;
20442044
Py_INCREF(coro);
20452045
Py_XSETREF(self->task_coro, coro);
20462046

@@ -2207,10 +2207,10 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg)
22072207
Py_RETURN_FALSE;
22082208
}
22092209

2210-
if (self->task_cancel_requested) {
2210+
self->task_num_cancels_requested += 1;
2211+
if (self->task_num_cancels_requested > 1) {
22112212
Py_RETURN_FALSE;
22122213
}
2213-
self->task_cancel_requested = 1;
22142214

22152215
if (self->task_fut_waiter) {
22162216
PyObject *res;
@@ -2256,12 +2256,7 @@ _asyncio_Task_cancelling_impl(TaskObj *self)
22562256
/*[clinic end generated code: output=803b3af96f917d7e input=c50e50f9c3ca4676]*/
22572257
/*[clinic end generated code]*/
22582258
{
2259-
if (self->task_cancel_requested) {
2260-
Py_RETURN_TRUE;
2261-
}
2262-
else {
2263-
Py_RETURN_FALSE;
2264-
}
2259+
return PyLong_FromLong(self->task_num_cancels_requested);
22652260
}
22662261

22672262
/*[clinic input]
@@ -2280,13 +2275,10 @@ _asyncio_Task_uncancel_impl(TaskObj *self)
22802275
/*[clinic end generated code: output=58184d236a817d3c input=5db95e28fcb6f7cd]*/
22812276
/*[clinic end generated code]*/
22822277
{
2283-
if (self->task_cancel_requested) {
2284-
self->task_cancel_requested = 0;
2285-
Py_RETURN_TRUE;
2286-
}
2287-
else {
2288-
Py_RETURN_FALSE;
2278+
if (self->task_num_cancels_requested > 0) {
2279+
self->task_num_cancels_requested -= 1;
22892280
}
2281+
return PyLong_FromLong(self->task_num_cancels_requested);
22902282
}
22912283

22922284
/*[clinic input]

0 commit comments

Comments
 (0)