Running an async background task in Tornado

前端 未结 5 1410
北荒
北荒 2020-12-15 06:50

Reading the Tornado documentation, it\'s very clear how to call an async function to return a response:

class GenAsyncHandler(RequestHandler):
    @gen.corou         


        
相关标签:
5条回答
  • 2020-12-15 07:09

    Here's my answer in 2019!

    Start with some slow non-blocking code. See: http://www.tornadoweb.org/en/stable/faq.html#id2

    async def _do_slow_task(self, pk):
        await asyncio.sleep(pk)
        logger.info(f'Finished slow task after {pk} seconds')
    

    See here to understand the difference between async and blocking: http://www.tornadoweb.org/en/stable/guide/async.html#asynchronous-and-non-blocking-i-o

    Then, make your request method a coroutine with the async/await syntax, which makes it non-blocking in order to handle multiple requests in parallel.

    async def post(self):
        """Make a few requests with different pks and you should see that
            the numbers logged are in ascending order.
        """
        pk = self.get_query_argument('pk')
        try:
            record = await self.db_queryone(
                f"SELECT * FROM records WHERE id = {int(pk)};"
            )
        except Exception as e:
            self.set_status(400)
            self.write(str(e))
            return
        await self._do_slow_task(pk)
        self.write(f'Received {pk}')
    

    Now, modify the method a bit to run in the background or

    “fire and forget” a coroutine without waiting for its result

    so that the client receives a response right away. See: http://www.tornadoweb.org/en/stable/guide/coroutines.html#how-to-call-a-coroutine

    async def post(self):
        """Make a few requests with different pks and you should see responses
            right away, and eventually log messages with the numbers in
            ascending order.
        """
        pk = self.get_query_argument('pk')
        try:
            record = await self.db_queryone(
                f"SELECT * FROM records WHERE id = {int(pk)};"
            )
        except Exception as e:
            self.set_status(400)
            self.write(str(e))
            return
        IOLoop.current().spawn_callback(self._do_slow_task, pk)
        self.write(f'Received {pk}')
    
    0 讨论(0)
  • 2020-12-15 07:13

    I recommend using toro. It provides a relatively simple mechanism for setting up a background queue of tasks.

    The following code (put in queue.py for example), starts a simple "worker()" that simply waits until there is something in his queue. If you call queue.add(function,async,*args,**kwargs) this adds an item to the queue which will wake up worker() which then kicks off the task.

    I added the async parameter so that this can support background tasks wrapped in @gen.coroutine and those without.

    import toro,tornado.gen
    queue = toro.Queue()
    @tornado.gen.coroutine
    def add(function,async,*args,**kwargs):
       item = dict(function=function,async=async,args=args,kwargs=kwargs)
       yield queue.put(item)
    
    @tornado.gen.coroutine
    def worker():
       while True:
          print("worker() sleeping until I get next item")
          item = yield queue.get()
          print("worker() waking up to process: %s" % item)
          try:
             if item['async']:
                yield item['function'](*item['args'],**item['kwargs'])
             else:
                item['function'](*item['args'],**item['kwargs'])
          except Exception as e:
             print("worker() failed to run item: %s, received exception:\n%s" % (item,e))
    
    @tornado.gen.coroutine
    def start():
       yield worker()
    

    In your main tornado app:

    import queue
    queue.start()
    

    And now you can schedule a back ground task quite simply:

    def my_func(arg1,somekwarg=None):
       print("in my_func() with %s %s" % (arg1,somekwarg))
    
    queue.add(my_func,False,somearg,somekwarg=someval)
    
    0 讨论(0)
  • 2020-12-15 07:20

    I have a time-consuming task in post request, maybe more than 30 minutes need, but client required return a result immediately.

    First, I used IOLoop.current().spawn_callback. It works! but! If the first request task is running, second request task blocked! Because all tasks are in main event loop when use spawn_callback, so one task is synchronous execution, other tasks blocked.

    Last, I use tornado.concurrent. Example:

    import datetime
    import time
    
    from tornado.ioloop import IOLoop
    import tornado.web
    from tornado import concurrent
    
    executor = concurrent.futures.ThreadPoolExecutor(8)
    
    
    class Handler(tornado.web.RequestHandler):
    
        def get(self):
            def task(arg):
                for i in range(10):
                    time.sleep(1)
                    print(arg, i)
    
            executor.submit(task, datetime.datetime.now())
            self.write('request accepted')
    
    
    def make_app():
        return tornado.web.Application([
            (r"/", Handler),
        ])
    
    
    if __name__ == "__main__":
        app = make_app()
        app.listen(8000, '0.0.0.0')
        IOLoop.current().start()
    

    and visit http://127.0.0.1:8000, you can see it's run ok:

    2017-01-17 22:42:10.983632 0
    2017-01-17 22:42:10.983632 1
    2017-01-17 22:42:10.983632 2
    2017-01-17 22:42:13.710145 0
    2017-01-17 22:42:10.983632 3
    2017-01-17 22:42:13.710145 1
    2017-01-17 22:42:10.983632 4
    2017-01-17 22:42:13.710145 2
    2017-01-17 22:42:10.983632 5
    2017-01-17 22:42:16.694966 0
    2017-01-17 22:42:13.710145 3
    2017-01-17 22:42:10.983632 6
    2017-01-17 22:42:16.694966 1
    2017-01-17 22:42:13.710145 4
    2017-01-17 22:42:10.983632 7
    2017-01-17 22:42:16.694966 2
    2017-01-17 22:42:13.710145 5
    2017-01-17 22:42:10.983632 8
    2017-01-17 22:42:16.694966 3
    2017-01-17 22:42:13.710145 6
    2017-01-17 22:42:19.790646 0
    2017-01-17 22:42:10.983632 9
    2017-01-17 22:42:16.694966 4
    2017-01-17 22:42:13.710145 7
    2017-01-17 22:42:19.790646 1
    2017-01-17 22:42:16.694966 5
    2017-01-17 22:42:13.710145 8
    2017-01-17 22:42:19.790646 2
    2017-01-17 22:42:16.694966 6
    2017-01-17 22:42:13.710145 9
    2017-01-17 22:42:19.790646 3
    2017-01-17 22:42:16.694966 7
    2017-01-17 22:42:19.790646 4
    2017-01-17 22:42:16.694966 8
    2017-01-17 22:42:19.790646 5
    2017-01-17 22:42:16.694966 9
    2017-01-17 22:42:19.790646 6
    2017-01-17 22:42:19.790646 7
    2017-01-17 22:42:19.790646 8
    2017-01-17 22:42:19.790646 9
    

    Want to help everyone!

    0 讨论(0)
  • 2020-12-15 07:23

    Update: Since Tornado 4.0 (July 2014), the below functionality is available in the IOLoop.spawn_callback method.

    Unfortunately it's kind of tricky. You need to both detach the background task from the current request (so that a failure in the background task doesn't result in a random exception thrown into the request) and ensure that something is listening to the background task's result (to log its errors if nothing else). This means something like this:

    from tornado.ioloop import IOLoop
    from tornado.stack_context import run_in_stack_context, NullContext
    IOLoop.current().add_future(run_in_stack_context(NullContext(), self._background_task),
                                lambda f: f.result())
    

    Something like this will probably be added to tornado itself in the future.

    0 讨论(0)
  • 2020-12-15 07:23

    Simply do:

    self._background_task()
    

    The _background_task coroutine returns a Future which is unresolved until the coroutine completes. If you don't yield the Future, and instead simply execute the next line immediately, then get() doesn't wait for _background_task to finish.

    An interesting detail is that, until _background_task finishes, it maintains a reference to self. (Don't forget to add self as a parameter, by the way.) Your RequestHandler won't be garbage collected until after _background_task completes.

    0 讨论(0)
提交回复
热议问题