Commit 80f53aa9 authored by Victor Stinner's avatar Victor Stinner

asyncio, Tulip issue 137: In debug mode, save traceback where Future, Task and

Handle objects are created. Pass the traceback to call_exception_handler() in
the 'source_traceback' key.

The traceback is truncated to hide internal calls in asyncio, show only the
traceback from user code.

Add tests for the new source_traceback, and a test for the 'Future/Task
exception was never retrieved' log.
parent bbd96c6f
...@@ -21,6 +21,7 @@ import inspect ...@@ -21,6 +21,7 @@ import inspect
import logging import logging
import socket import socket
import subprocess import subprocess
import traceback
import time import time
import os import os
import sys import sys
...@@ -290,7 +291,10 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -290,7 +291,10 @@ class BaseEventLoop(events.AbstractEventLoop):
Any positional arguments after the callback will be passed to Any positional arguments after the callback will be passed to
the callback when it is called. the callback when it is called.
""" """
return self.call_at(self.time() + delay, callback, *args) timer = self.call_at(self.time() + delay, callback, *args)
if timer._source_traceback:
del timer._source_traceback[-1]
return timer
def call_at(self, when, callback, *args): def call_at(self, when, callback, *args):
"""Like call_later(), but uses an absolute time.""" """Like call_later(), but uses an absolute time."""
...@@ -299,6 +303,8 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -299,6 +303,8 @@ class BaseEventLoop(events.AbstractEventLoop):
if self._debug: if self._debug:
self._assert_is_current_event_loop() self._assert_is_current_event_loop()
timer = events.TimerHandle(when, callback, args, self) timer = events.TimerHandle(when, callback, args, self)
if timer._source_traceback:
del timer._source_traceback[-1]
heapq.heappush(self._scheduled, timer) heapq.heappush(self._scheduled, timer)
return timer return timer
...@@ -312,7 +318,10 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -312,7 +318,10 @@ class BaseEventLoop(events.AbstractEventLoop):
Any positional arguments after the callback will be passed to Any positional arguments after the callback will be passed to
the callback when it is called. the callback when it is called.
""" """
return self._call_soon(callback, args, check_loop=True) handle = self._call_soon(callback, args, check_loop=True)
if handle._source_traceback:
del handle._source_traceback[-1]
return handle
def _call_soon(self, callback, args, check_loop): def _call_soon(self, callback, args, check_loop):
if tasks.iscoroutinefunction(callback): if tasks.iscoroutinefunction(callback):
...@@ -320,6 +329,8 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -320,6 +329,8 @@ class BaseEventLoop(events.AbstractEventLoop):
if self._debug and check_loop: if self._debug and check_loop:
self._assert_is_current_event_loop() self._assert_is_current_event_loop()
handle = events.Handle(callback, args, self) handle = events.Handle(callback, args, self)
if handle._source_traceback:
del handle._source_traceback[-1]
self._ready.append(handle) self._ready.append(handle)
return handle return handle
...@@ -344,6 +355,8 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -344,6 +355,8 @@ class BaseEventLoop(events.AbstractEventLoop):
def call_soon_threadsafe(self, callback, *args): def call_soon_threadsafe(self, callback, *args):
"""Like call_soon(), but thread safe.""" """Like call_soon(), but thread safe."""
handle = self._call_soon(callback, args, check_loop=False) handle = self._call_soon(callback, args, check_loop=False)
if handle._source_traceback:
del handle._source_traceback[-1]
self._write_to_self() self._write_to_self()
return handle return handle
...@@ -757,7 +770,14 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -757,7 +770,14 @@ class BaseEventLoop(events.AbstractEventLoop):
for key in sorted(context): for key in sorted(context):
if key in {'message', 'exception'}: if key in {'message', 'exception'}:
continue continue
log_lines.append('{}: {!r}'.format(key, context[key])) value = context[key]
if key == 'source_traceback':
tb = ''.join(traceback.format_list(value))
value = 'Object created at (most recent call last):\n'
value += tb.rstrip()
else:
value = repr(value)
log_lines.append('{}: {}'.format(key, value))
logger.error('\n'.join(log_lines), exc_info=exc_info) logger.error('\n'.join(log_lines), exc_info=exc_info)
......
...@@ -11,6 +11,7 @@ __all__ = ['AbstractEventLoopPolicy', ...@@ -11,6 +11,7 @@ __all__ = ['AbstractEventLoopPolicy',
import functools import functools
import inspect import inspect
import subprocess import subprocess
import traceback
import threading import threading
import socket import socket
import sys import sys
...@@ -66,7 +67,8 @@ def _format_callback(func, args, suffix=''): ...@@ -66,7 +67,8 @@ def _format_callback(func, args, suffix=''):
class Handle: class Handle:
"""Object returned by callback registration methods.""" """Object returned by callback registration methods."""
__slots__ = ['_callback', '_args', '_cancelled', '_loop', '__weakref__'] __slots__ = ('_callback', '_args', '_cancelled', '_loop',
'_source_traceback', '__weakref__')
def __init__(self, callback, args, loop): def __init__(self, callback, args, loop):
assert not isinstance(callback, Handle), 'A Handle is not a callback' assert not isinstance(callback, Handle), 'A Handle is not a callback'
...@@ -74,6 +76,10 @@ class Handle: ...@@ -74,6 +76,10 @@ class Handle:
self._callback = callback self._callback = callback
self._args = args self._args = args
self._cancelled = False self._cancelled = False
if self._loop.get_debug():
self._source_traceback = traceback.extract_stack(sys._getframe(1))
else:
self._source_traceback = None
def __repr__(self): def __repr__(self):
info = [] info = []
...@@ -91,11 +97,14 @@ class Handle: ...@@ -91,11 +97,14 @@ class Handle:
except Exception as exc: except Exception as exc:
cb = _format_callback(self._callback, self._args) cb = _format_callback(self._callback, self._args)
msg = 'Exception in callback {}'.format(cb) msg = 'Exception in callback {}'.format(cb)
self._loop.call_exception_handler({ context = {
'message': msg, 'message': msg,
'exception': exc, 'exception': exc,
'handle': self, 'handle': self,
}) }
if self._source_traceback:
context['source_traceback'] = self._source_traceback
self._loop.call_exception_handler(context)
self = None # Needed to break cycles when an exception occurs. self = None # Needed to break cycles when an exception occurs.
...@@ -107,7 +116,8 @@ class TimerHandle(Handle): ...@@ -107,7 +116,8 @@ class TimerHandle(Handle):
def __init__(self, when, callback, args, loop): def __init__(self, when, callback, args, loop):
assert when is not None assert when is not None
super().__init__(callback, args, loop) super().__init__(callback, args, loop)
if self._source_traceback:
del self._source_traceback[-1]
self._when = when self._when = when
def __repr__(self): def __repr__(self):
......
...@@ -82,10 +82,11 @@ class _TracebackLogger: ...@@ -82,10 +82,11 @@ class _TracebackLogger:
in a discussion about closing files when they are collected. in a discussion about closing files when they are collected.
""" """
__slots__ = ['exc', 'tb', 'loop'] __slots__ = ('loop', 'source_traceback', 'exc', 'tb')
def __init__(self, exc, loop): def __init__(self, future, exc):
self.loop = loop self.loop = future._loop
self.source_traceback = future._source_traceback
self.exc = exc self.exc = exc
self.tb = None self.tb = None
...@@ -102,11 +103,12 @@ class _TracebackLogger: ...@@ -102,11 +103,12 @@ class _TracebackLogger:
def __del__(self): def __del__(self):
if self.tb: if self.tb:
msg = 'Future/Task exception was never retrieved:\n{tb}' msg = 'Future/Task exception was never retrieved'
context = { if self.source_traceback:
'message': msg.format(tb=''.join(self.tb)), msg += '\nFuture/Task created at (most recent call last):\n'
} msg += ''.join(traceback.format_list(self.source_traceback))
self.loop.call_exception_handler(context) msg += ''.join(self.tb).rstrip()
self.loop.call_exception_handler({'message': msg})
class Future: class Future:
...@@ -149,6 +151,10 @@ class Future: ...@@ -149,6 +151,10 @@ class Future:
else: else:
self._loop = loop self._loop = loop
self._callbacks = [] self._callbacks = []
if self._loop.get_debug():
self._source_traceback = traceback.extract_stack(sys._getframe(1))
else:
self._source_traceback = None
def _format_callbacks(self): def _format_callbacks(self):
cb = self._callbacks cb = self._callbacks
...@@ -196,10 +202,13 @@ class Future: ...@@ -196,10 +202,13 @@ class Future:
return return
exc = self._exception exc = self._exception
context = { context = {
'message': 'Future/Task exception was never retrieved', 'message': ('%s exception was never retrieved'
% self.__class__.__name__),
'exception': exc, 'exception': exc,
'future': self, 'future': self,
} }
if self._source_traceback:
context['source_traceback'] = self._source_traceback
self._loop.call_exception_handler(context) self._loop.call_exception_handler(context)
def cancel(self): def cancel(self):
...@@ -335,7 +344,7 @@ class Future: ...@@ -335,7 +344,7 @@ class Future:
if _PY34: if _PY34:
self._log_traceback = True self._log_traceback = True
else: else:
self._tb_logger = _TracebackLogger(exception, self._loop) self._tb_logger = _TracebackLogger(self, exception)
# Arrange for the logger to be activated after all callbacks # Arrange for the logger to be activated after all callbacks
# have had a chance to call result() or exception(). # have had a chance to call result() or exception().
self._loop.call_soon(self._tb_logger.activate) self._loop.call_soon(self._tb_logger.activate)
......
...@@ -195,6 +195,8 @@ class Task(futures.Future): ...@@ -195,6 +195,8 @@ class Task(futures.Future):
def __init__(self, coro, *, loop=None): def __init__(self, coro, *, loop=None):
assert iscoroutine(coro), repr(coro) # Not a coroutine function! assert iscoroutine(coro), repr(coro) # Not a coroutine function!
super().__init__(loop=loop) super().__init__(loop=loop)
if self._source_traceback:
del self._source_traceback[-1]
self._coro = iter(coro) # Use the iterator just in case. self._coro = iter(coro) # Use the iterator just in case.
self._fut_waiter = None self._fut_waiter = None
self._must_cancel = False self._must_cancel = False
...@@ -207,10 +209,13 @@ class Task(futures.Future): ...@@ -207,10 +209,13 @@ class Task(futures.Future):
if _PY34: if _PY34:
def __del__(self): def __del__(self):
if self._state == futures._PENDING: if self._state == futures._PENDING:
self._loop.call_exception_handler({ context = {
'task': self, 'task': self,
'message': 'Task was destroyed but it is pending!', 'message': 'Task was destroyed but it is pending!',
}) }
if self._source_traceback:
context['source_traceback'] = self._source_traceback
self._loop.call_exception_handler(context)
futures.Future.__del__(self) futures.Future.__del__(self)
def __repr__(self): def __repr__(self):
...@@ -620,7 +625,10 @@ def async(coro_or_future, *, loop=None): ...@@ -620,7 +625,10 @@ def async(coro_or_future, *, loop=None):
raise ValueError('loop argument must agree with Future') raise ValueError('loop argument must agree with Future')
return coro_or_future return coro_or_future
elif iscoroutine(coro_or_future): elif iscoroutine(coro_or_future):
return Task(coro_or_future, loop=loop) task = Task(coro_or_future, loop=loop)
if task._source_traceback:
del task._source_traceback[-1]
return task
else: else:
raise TypeError('A Future or coroutine is required') raise TypeError('A Future or coroutine is required')
......
...@@ -406,19 +406,22 @@ class BaseEventLoopTests(test_utils.TestCase): ...@@ -406,19 +406,22 @@ class BaseEventLoopTests(test_utils.TestCase):
1/0 1/0
def run_loop(): def run_loop():
self.loop.call_soon(zero_error) handle = self.loop.call_soon(zero_error)
self.loop._run_once() self.loop._run_once()
return handle
self.loop.set_debug(True)
self.loop._process_events = mock.Mock() self.loop._process_events = mock.Mock()
mock_handler = mock.Mock() mock_handler = mock.Mock()
self.loop.set_exception_handler(mock_handler) self.loop.set_exception_handler(mock_handler)
run_loop() handle = run_loop()
mock_handler.assert_called_with(self.loop, { mock_handler.assert_called_with(self.loop, {
'exception': MOCK_ANY, 'exception': MOCK_ANY,
'message': test_utils.MockPattern( 'message': test_utils.MockPattern(
'Exception in callback.*zero_error'), 'Exception in callback.*zero_error'),
'handle': MOCK_ANY, 'handle': handle,
'source_traceback': handle._source_traceback,
}) })
mock_handler.reset_mock() mock_handler.reset_mock()
......
...@@ -1751,10 +1751,11 @@ def noop(*args): ...@@ -1751,10 +1751,11 @@ def noop(*args):
pass pass
class HandleTests(unittest.TestCase): class HandleTests(test_utils.TestCase):
def setUp(self): def setUp(self):
self.loop = None self.loop = mock.Mock()
self.loop.get_debug.return_value = True
def test_handle(self): def test_handle(self):
def callback(*args): def callback(*args):
...@@ -1789,7 +1790,8 @@ class HandleTests(unittest.TestCase): ...@@ -1789,7 +1790,8 @@ class HandleTests(unittest.TestCase):
self.loop.call_exception_handler.assert_called_with({ self.loop.call_exception_handler.assert_called_with({
'message': test_utils.MockPattern('Exception in callback.*'), 'message': test_utils.MockPattern('Exception in callback.*'),
'exception': mock.ANY, 'exception': mock.ANY,
'handle': h 'handle': h,
'source_traceback': h._source_traceback,
}) })
def test_handle_weakref(self): def test_handle_weakref(self):
...@@ -1837,6 +1839,35 @@ class HandleTests(unittest.TestCase): ...@@ -1837,6 +1839,35 @@ class HandleTests(unittest.TestCase):
% (cb_regex, re.escape(filename), lineno)) % (cb_regex, re.escape(filename), lineno))
self.assertRegex(repr(h), regex) self.assertRegex(repr(h), regex)
def test_handle_source_traceback(self):
loop = asyncio.get_event_loop_policy().new_event_loop()
loop.set_debug(True)
self.set_event_loop(loop)
def check_source_traceback(h):
lineno = sys._getframe(1).f_lineno - 1
self.assertIsInstance(h._source_traceback, list)
self.assertEqual(h._source_traceback[-1][:3],
(__file__,
lineno,
'test_handle_source_traceback'))
# call_soon
h = loop.call_soon(noop)
check_source_traceback(h)
# call_soon_threadsafe
h = loop.call_soon_threadsafe(noop)
check_source_traceback(h)
# call_later
h = loop.call_later(0, noop)
check_source_traceback(h)
# call_at
h = loop.call_later(0, noop)
check_source_traceback(h)
class TimerTests(unittest.TestCase): class TimerTests(unittest.TestCase):
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
import concurrent.futures import concurrent.futures
import re import re
import sys
import threading import threading
import unittest import unittest
from test import support
from unittest import mock from unittest import mock
import asyncio import asyncio
...@@ -284,6 +286,63 @@ class FutureTests(test_utils.TestCase): ...@@ -284,6 +286,63 @@ class FutureTests(test_utils.TestCase):
self.assertEqual(f1.result(), 42) self.assertEqual(f1.result(), 42)
self.assertTrue(f2.cancelled()) self.assertTrue(f2.cancelled())
def test_future_source_traceback(self):
self.loop.set_debug(True)
future = asyncio.Future(loop=self.loop)
lineno = sys._getframe().f_lineno - 1
self.assertIsInstance(future._source_traceback, list)
self.assertEqual(future._source_traceback[-1][:3],
(__file__,
lineno,
'test_future_source_traceback'))
@mock.patch('asyncio.base_events.logger')
def test_future_exception_never_retrieved(self, m_log):
self.loop.set_debug(True)
def memroy_error():
try:
raise MemoryError()
except BaseException as exc:
return exc
exc = memroy_error()
future = asyncio.Future(loop=self.loop)
source_traceback = future._source_traceback
future.set_exception(exc)
future = None
test_utils.run_briefly(self.loop)
support.gc_collect()
if sys.version_info >= (3, 4):
frame = source_traceback[-1]
regex = (r'^Future exception was never retrieved\n'
r'future: <Future finished exception=MemoryError\(\)>\n'
r'source_traceback: Object created at \(most recent call last\):\n'
r' File'
r'.*\n'
r' File "%s", line %s, in test_future_exception_never_retrieved\n'
r' future = asyncio\.Future\(loop=self\.loop\)$'
% (frame[0], frame[1]))
exc_info = (type(exc), exc, exc.__traceback__)
m_log.error.assert_called_once_with(mock.ANY, exc_info=exc_info)
else:
frame = source_traceback[-1]
regex = (r'^Future/Task exception was never retrieved\n'
r'Future/Task created at \(most recent call last\):\n'
r' File'
r'.*\n'
r' File "%s", line %s, in test_future_exception_never_retrieved\n'
r' future = asyncio\.Future\(loop=self\.loop\)\n'
r'Traceback \(most recent call last\):\n'
r'.*\n'
r'MemoryError$'
% (frame[0], frame[1]))
m_log.error.assert_called_once_with(mock.ANY, exc_info=False)
message = m_log.error.call_args[0][0]
self.assertRegex(message, re.compile(regex, re.DOTALL))
class FutureDoneCallbackTests(test_utils.TestCase): class FutureDoneCallbackTests(test_utils.TestCase):
......
...@@ -1546,6 +1546,7 @@ class TaskTests(test_utils.TestCase): ...@@ -1546,6 +1546,7 @@ class TaskTests(test_utils.TestCase):
raise Exception("code never reached") raise Exception("code never reached")
mock_handler = mock.Mock() mock_handler = mock.Mock()
self.loop.set_debug(True)
self.loop.set_exception_handler(mock_handler) self.loop.set_exception_handler(mock_handler)
# schedule the task # schedule the task
...@@ -1560,6 +1561,7 @@ class TaskTests(test_utils.TestCase): ...@@ -1560,6 +1561,7 @@ class TaskTests(test_utils.TestCase):
# remove the future used in kill_me(), and references to the task # remove the future used in kill_me(), and references to the task
del coro.gi_frame.f_locals['future'] del coro.gi_frame.f_locals['future']
coro = None coro = None
source_traceback = task._source_traceback
task = None task = None
# no more reference to kill_me() task: the task is destroyed by the GC # no more reference to kill_me() task: the task is destroyed by the GC
...@@ -1570,6 +1572,7 @@ class TaskTests(test_utils.TestCase): ...@@ -1570,6 +1572,7 @@ class TaskTests(test_utils.TestCase):
mock_handler.assert_called_with(self.loop, { mock_handler.assert_called_with(self.loop, {
'message': 'Task was destroyed but it is pending!', 'message': 'Task was destroyed but it is pending!',
'task': mock.ANY, 'task': mock.ANY,
'source_traceback': source_traceback,
}) })
mock_handler.reset_mock() mock_handler.reset_mock()
...@@ -1604,6 +1607,17 @@ class TaskTests(test_utils.TestCase): ...@@ -1604,6 +1607,17 @@ class TaskTests(test_utils.TestCase):
self.assertRegex(message, re.compile(regex, re.DOTALL)) self.assertRegex(message, re.compile(regex, re.DOTALL))
def test_task_source_traceback(self):
self.loop.set_debug(True)
task = asyncio.Task(coroutine_function(), loop=self.loop)
lineno = sys._getframe().f_lineno - 1
self.assertIsInstance(task._source_traceback, list)
self.assertEqual(task._source_traceback[-1][:3],
(__file__,
lineno,
'test_task_source_traceback'))
class GatherTestsBase: class GatherTestsBase:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment