Hi,
Develop with asyncio is hard because unhandled exceptions are delayed.
If there is still a reference to the future with the unhandled
exception, the unhandled exception will never be logger. If the future
is destroyed, the unhandled exception is logged, but only where the
exception was raised, not where the future was created. Sometimes, you
need both info to understand an issue.
Attached patch is a simple demo to using the tracemalloc (new in
Python 3.4) to show the traceback where a future was created when the
"unhandled exception" message is logger.
The "creation traceback" can also be used in CoroWrapper destructor to
get the whole traceback where a coroutine was created, not only where
the coroutine function was declared.
tracemalloc is just used to show the expected output, but I would
prefer to record manually the traceback where a taks was created in
Future constructor because tracemalloc has an important cost on the
global memory footprint and global performances. For Python 3.3 and
older, you need also to recompile your own Python interpreter and then
compile tracemalloc to get tracemalloc, it's not very convinient.
Example raising an exception which will not be handled:
---
import asyncio
@asyncio.coroutine
def bug():
raise Exception("not consumed") # line 5
loop = asyncio.get_event_loop()
asyncio.async(bug()) # line 8
loop.run_forever()
---
Output:
---
Future/Task exception was never retrieved:
Traceback (most recent call last):
File "asyncio/tasks.py", line 281, in _step
result = next(coro)
File "asyncio/tasks.py", line 82, in coro
res = func(*args, **kw)
File "test.py", line 5, in bug
raise Exception("not consumed")
Exception: not consumed
Future/Task created at:
File "asyncio/tasks.py", line 526
return Task(coro_or_future, loop=loop)
File "test.py", line 8
asyncio.async(bug())
---
In the output you can find the line 5 where the exception was raised
and the line 8 where the coroutine was created.
Victor
diff -r 9be31d11cb26 asyncio/futures.py
--- a/asyncio/futures.py Sun Feb 09 02:50:13 2014 +0100
+++ b/asyncio/futures.py Sun Feb 09 23:17:22 2014 +0100
@@ -9,6 +9,11 @@ import concurrent.futures._base
import logging
import sys
import traceback
+try:
+ import tracemalloc
+ import linecache
+except ImportError:
+ tracemalloc = None
from . import events
from .log import logger
@@ -175,6 +180,17 @@ class Future:
exc = self._exception
logger.error('Future/Task exception was never retrieved:',
exc_info=(exc.__class__, exc, exc.__traceback__))
+ if tracemalloc is not None:
+ traceback = tracemalloc.get_object_traceback(self)
+ else:
+ traceback = None
+ if traceback:
+ logger.error("Future/Task created at:")
+ for frame in traceback:
+ logger.error(' File "%s", line %s', frame.filename, frame.lineno)
+ line = linecache.getline(frame.filename, frame.lineno).strip()
+ if line:
+ logger.error(' %s', line)
def cancel(self):
"""Cancel the future and schedule callbacks.