Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a02f81f

Browse files
committed
asyncio: Log an error if a Task is destroyed while it is still pending
1 parent 4c945fe commit a02f81f

4 files changed

Lines changed: 60 additions & 4 deletions

File tree

Lib/asyncio/futures.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ def __repr__(self):
169169
res += '<{}>'.format(self._state)
170170
return res
171171

172+
# On Python 3.3 or older, objects with a destructor part of a reference
173+
# cycle are never destroyed. It's not more the case on Python 3.4 thanks to
174+
# the PEP 442.
172175
if _PY34:
173176
def __del__(self):
174177
if not self._log_traceback:

Lib/asyncio/tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
_DEBUG = (not sys.flags.ignore_environment
3333
and bool(os.environ.get('PYTHONASYNCIODEBUG')))
3434

35+
_PY34 = (sys.version_info >= (3, 4))
3536
_PY35 = (sys.version_info >= (3, 5))
3637

3738

@@ -181,6 +182,18 @@ def __init__(self, coro, *, loop=None):
181182
self._loop.call_soon(self._step)
182183
self.__class__._all_tasks.add(self)
183184

185+
# On Python 3.3 or older, objects with a destructor part of a reference
186+
# cycle are never destroyed. It's not more the case on Python 3.4 thanks to
187+
# the PEP 442.
188+
if _PY34:
189+
def __del__(self):
190+
if self._state == futures._PENDING:
191+
self._loop.call_exception_handler({
192+
'task': self,
193+
'message': 'Task was destroyed but it is pending!',
194+
})
195+
futures.Future.__del__(self)
196+
184197
def __repr__(self):
185198
res = super().__repr__()
186199
if (self._must_cancel and

Lib/test/test_asyncio/test_base_events.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ def test_set_debug(self):
244244
@mock.patch('asyncio.base_events.logger')
245245
def test__run_once_logging(self, m_logger):
246246
def slow_select(timeout):
247-
time.sleep(1.0)
247+
# Sleep a bit longer than a second to avoid timer resolution issues.
248+
time.sleep(1.1)
248249
return []
249250

250251
# logging needs debug flag

Lib/test/test_asyncio/test_tasks.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import types
66
import unittest
77
import weakref
8+
from test import support
89
from test.script_helper import assert_python_ok
10+
from unittest import mock
911

1012
import asyncio
1113
from asyncio import tasks
1214
from asyncio import test_utils
1315

1416

17+
PY34 = (sys.version_info >= (3, 4))
1518
PY35 = (sys.version_info >= (3, 5))
1619

1720

@@ -1501,9 +1504,45 @@ def call(arg):
15011504
def test_corowrapper_weakref(self):
15021505
wd = weakref.WeakValueDictionary()
15031506
def foo(): yield from []
1504-
cw = asyncio.tasks.CoroWrapper(foo(), foo)
1505-
wd['cw'] = cw # Would fail without __weakref__ slot.
1506-
cw.gen = None # Suppress warning from __del__.
1507+
1508+
@unittest.skipUnless(PY34,
1509+
'need python 3.4 or later')
1510+
def test_log_destroyed_pending_task(self):
1511+
@asyncio.coroutine
1512+
def kill_me(loop):
1513+
future = asyncio.Future(loop=loop)
1514+
yield from future
1515+
# at this point, the only reference to kill_me() task is
1516+
# the Task._wakeup() method in future._callbacks
1517+
raise Exception("code never reached")
1518+
1519+
mock_handler = mock.Mock()
1520+
self.loop.set_exception_handler(mock_handler)
1521+
1522+
# schedule the task
1523+
coro = kill_me(self.loop)
1524+
task = asyncio.async(coro, loop=self.loop)
1525+
self.assertEqual(asyncio.Task.all_tasks(loop=self.loop), {task})
1526+
1527+
# execute the task so it waits for future
1528+
self.loop._run_once()
1529+
self.assertEqual(len(self.loop._ready), 0)
1530+
1531+
# remove the future used in kill_me(), and references to the task
1532+
del coro.gi_frame.f_locals['future']
1533+
coro = None
1534+
task = None
1535+
1536+
# no more reference to kill_me() task: the task is destroyed by the GC
1537+
support.gc_collect()
1538+
1539+
self.assertEqual(asyncio.Task.all_tasks(loop=self.loop), set())
1540+
1541+
mock_handler.assert_called_with(self.loop, {
1542+
'message': 'Task was destroyed but it is pending!',
1543+
'task': mock.ANY,
1544+
})
1545+
mock_handler.reset_mock()
15071546

15081547

15091548
class GatherTestsBase:

0 commit comments

Comments
 (0)