8000 bpo-46771: cancel counts (#31434) · python/cpython@a926bf0 · GitHub
[go: up one dir, main page]

Skip to content

Commit a926bf0

Browse files
authored
bpo-46771: cancel counts (#31434)
1 parent ac90628 commit a926bf0

File tree

4 files changed

+77
-44
lines changed

4 files changed

+77
-44
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/asyncio/timeouts.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ async def __aexit__(
9090

9191
if self._state is _State.CANCELLING:
9292
self._state = _State.CANCELLED
93-
counter = _COUNTERS[self._task]
94-
if counter == 1:
93+
94+
if self._task.uncancel() == 0:
95+
# Since there are no outstanding cancel requests, we're
96+
# handling this.
9597
raise TimeoutError
96-
else:
97-
_COUNTERS[self._task] = counter - 1
9898
elif self._state is _State.ENTERED:
9999
self._state = _State.EXITED
100100

@@ -106,19 +106,6 @@ def _on_timeout(self) -> None:
106106
self._state = _State.CANCELLING
107107
# drop the reference early
108108
self._timeout_handler = None
109-
counter = _COUNTERS.get(self._task)
110-
if counter is None:
111-
_COUNTERS[self._task] = 1
112-
self._task.add_done_callback(_drop_task)
113-
else:
114-
_COUNTERS[self._task] = counter + 1
115-
116-
117-
_COUNTERS: Dict[tasks.Task, int] = {}
118-
119-
120-
def _drop_task(task: tasks.Task) -> None:
121-
del _COUNTERS[task]
122109

123110

124111
def timeout(delay: Optional[float]) -> Timeout:

Lib/test/test_asyncio/test_timeouts.py

Lines changed: 56 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,17 @@ 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+
1931
async def test_timeout_basic(self):
2032
with self.assertRaises(TimeoutError):
2133
async with asyncio.timeout(0.01) as cm:
@@ -137,6 +149,50 @@ async def outer() -> None:
137149
assert not task.cancelled()
138150
assert task.done()
139151

152+
async def test_nested_timeouts(self):
153+
with self.assertRaises(TimeoutError):
154+
async with asyncio.timeout(0.1) as outer:
155+
try:
156+
async with asyncio.timeout(0.2) as inner:
157+
await asyncio.sleep(10)
158+
except asyncio.TimeoutError:
159+
# Pretend we start a super long operation here.
160+
self.assertTrue(False)
161+
162+
async def test_nested_timeouts_concurrent(self):
163+
with self.assertRaises(TimeoutError):
164+
async with asyncio.timeout(0.002):
165+
try:
166+
async with asyncio.timeout(0.003):
167+
# Pretend we crunch some numbers.
168+
time.sleep(0.005)
169+
await asyncio.sleep(1)
170+
except asyncio.TimeoutError:
171+
pass
172+
173+
async def test_nested_timeouts_loop_busy(self):
174+
"""
175+
After the inner timeout is an expensive operation which should
176+
be stopped by the outer timeout.
177+
178+
Note: this fails for now.
179+
"""
180+
start = time.perf_counter()
181+
try:
182+
async with asyncio.timeout(0.002) as outer:
183+
try:
184+
async with asyncio.timeout(0.001) as inner:
185+
# Pretend the loop is busy for a while.
186+
time.sleep(0.010)
187+
await asyncio.sleep(0.001)
188+
except asyncio.TimeoutError:
189+
# This sleep should be interrupted.
190+
await asyncio.sleep(0.050)
191+
except asyncio.TimeoutError:
192+
pass
193+
took = time.perf_counter() - start
194+
self.assertTrue(took <= 0.015)
195+
140196

141197
@unittest.skipUnless(hasattr(tasks, '_CTask'),
142198
'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)
0