3

I am trying to find a general solution for offloading blocking tasks to a ThreadPoolExecutor.

In the example below, I can achieve the desired non-blocking result in the NonBlockingHandler using Tornado's run_on_executor decorator.

In the asyncify decorator, I am trying to accomplish the same thing, but it blocks other calls.

Any ideas how to get the asyncify decorator to work correctly and not cause the decorated function to block?

NOTE: I am using Python 3.6.8 and Tornado 4.5.3

Here is the full working example:

import asyncio
from concurrent.futures import ThreadPoolExecutor
import functools
import time

from tornado.concurrent import Future,  run_on_executor
import tornado.ioloop
import tornado.web


def asyncify(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        result = Future()

        def _run_on_executor(fn):
            @functools.wraps(fn)
            def wrapper(*args, **kwargs):
                with ThreadPoolExecutor(max_workers=1) as executor:
                    return executor.submit(fn, *args, **kwargs)
            return wrapper

        @_run_on_executor
        def _func(*args, **kwargs):
            return func(*args, **kwargs)

        def on_response(response):
           result.set_result(response.result())

        future = _func(*args, **kwargs)
        future.add_done_callback(on_response)

        return await result
    return wrapper


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write(f"Hello, {self.__class__.__name__}")


class AsyncifyHandler(tornado.web.RequestHandler):
    @asyncify
    def get(self):
        print("sleeping")
        time.sleep(5)
        self.write(f"Hello, {self.__class__.__name__}")


class NonBlockingHandler(tornado.web.RequestHandler):
    executor = ThreadPoolExecutor(max_workers=1)

    @run_on_executor
    def blocking(self):
        print("sleeping")
        time.sleep(5)
        self.write(f"Hello, {self.__class__.__name__}")

    async def get(self):
        result = Future()
        publish = self.blocking()
        publish.add_done_callback(
            lambda response: result.set_result(response.result())
        )
        return await result


def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/asyncify", AsyncifyHandler),
        (r"/noblock", NonBlockingHandler),
    ], debug=True)


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
jdesilvio
  • 1,794
  • 4
  • 22
  • 38

1 Answers1

3

Exiting a with ThreadPoolExecutor block waits (synchronously!) for all tasks on the executor to finish. You can't shut down an executor while the IOLoop is running; just make a global run and let it run forever.

Ben Darnell
  • 21,844
  • 3
  • 29
  • 50
  • I got it to work by changing the wrapper to: `executor = ThreadPoolExecutor(max_workers=1);` `return executor.submit(fn, *args, **kwargs)` – jdesilvio Aug 29 '19 at 12:54