Search code examples
pythonasynchronoustornado

Tornado asynchronous callback exception


I have an asynchronous piece of code that executed concurrently:

@tornado.web.asynchronous
def execute(self, func, *args):
    def callback(future):
        try:
            to_return = json.dumps(future.result())
            self.write(to_return)
            self.flush()
            self.finish()
        except:
            error = error = self.get_exception()
            self.set_status(500)
            self.finish(json.dumps({"error": error))

    EXECUTOR.submit(
        partial(func, *args)
    ).add_done_callback(
        lambda future: tornado.ioloop.IOLoop.instance().add_callback(
            partial(callback, future)))


def get_exception(self):
    exc_type, exc_obj, tb = sys.exc_info()
    f = tb.tb_frame
    lineno = tb.tb_lineno
    filename = f.f_code.co_filename
    linecache.checkcache(filename)
    line = linecache.getline(filename, lineno, f.f_globals)
    return {'error_msg': 'LINE {} "{}"): {}, {}'.format(filename, lineno, line.strip(), exc_obj, exc_obj)}

This works well, except if an execution is thrown somewhere in the function, the stack trace only goes back to the point where it was thrown in the callback (ie where the future was executed), not the where it actually happened in the code.

Is it possible to capture the exception in the actually function where it was originally thrown?


Solution

  • In python 3, it should just work; in python 2 you're out of luck although I think you could write a wrapper around ThreadPoolExecutor that would capture more detail.

    Python's exception/traceback model changed between Python 2 and 3: In python 3, Exception objects carry their own traceback information, while in python 2 the traceback is a separate object (normally accessed via the sys.exc_info triple). Since the concurrent.futures package is backported from Python 3, it only captures the exception itself, not the exc_info triple, so the traceback is lost.

    Tornado's future class (tornado.concurrent.Future) has some extensions to capture the traceback in python 2 (the set_exc_info() method). I think you could do something like this (untested) to replace executor.submit:

    def submit_with_traceback(executor, func, *args):
        tornado_future = tornado.concurrent.Future()
        def wrapper():
            try:
                result = func(*args)
            except:
                tornado.future.set_exc_info(sys.exc_info())
            tornado_future.set_result(result)
        # executor.submit() returns a concurrent.futures.Future; ignore it
        executor.submit(wrapper)
        return tornado_future