Search code examples
pythontornadopython-asyncio

Run Tornado alongside another asyncio long-running task


I would like to run a Tornado server alongside an independent long-running task in asyncio in Python 3.7. I'm new to asyncio. I've read that you should only call asyncio.run() once, so I've brought both tasks together under the main() method so that I can pass a single argument to asyncio.run(). When I run this code, I get the error TypeError: a coroutine was expected, got <function start_tornado at 0x105c8e6a8>. I'd like to get the code to run without errors, but ultimately I want to know how to do this the right way. The code I've written below feels like an ugly hack.

import asyncio
import datetime
import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

# Fake background task I got from here:
# https://docs.python.org/3/library/asyncio-task.html#sleeping
async def display_date():
    while True:
        print(datetime.datetime.now())
        await asyncio.sleep(1)

async def start_tornado():
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

async def main():
    await asyncio.create_task(start_tornado)
    await asyncio.create_task(display_date)

asyncio.run(main())

Solution

    1. When you use create_task with an async def function, call the function normally and then pass the result to create_task.

      await asyncio.create_task(start_tornado())
      await asyncio.create_task(display_date())
      
    2. You don't need to use create_task if you're going to await it immediately. Use create_task without await to start tasks in the background, like display_date(). start_tornado is not a background task in this sense because it doesn't have an infinite loop, it just starts a server that is put into the background by Tornado. So I'd write it like this:

      await start_tornado()
      asyncio.create_task(display_date())
      
    3. Since Tornado 5.0, the Tornado IOLoop and asyncio event loop are integrated by default, so you only need to start one, not both. So just remove the IOLoop.start() call in start_tornado.

    4. start_tornado isn't currently doing anything asynchronous, so it could just be a normal function. But it would also be a reasonable place to add asynchronous startup logic like establishing database connections, so you can keep it as a coroutine.

    Working version of the code with my edits: https://repl.it/@bdarnell/FarawayAdmiredConversions