Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using a simple python generator as a co-routine in a Tornado async handler?

I have a python generator function which yields chunks of text. I would like to write a get method for a tornado.web.RequestHandler subclass that will iterate over the generator, writing the chunks out to the response as it goes.

Since this is Tornado, and since the generator may take over a second to process, I thought it would be nice to make the handler asynchronous, using this generator as a co-routine and passing off control to the IOLoop after every chunk. However, I can't make heads or tails of how to do this.

Here's my example (blocking) code:

class TextHandler(web.RequestHandler):
    @web.asynchronous
    def get(self, n):
        generator = self.generate_text(100000)
        # Clearly, this will block. How to make it asynchronous?
        for text in generator:
            self.write(text)

    def generate_text(n):
        for x in xrange(n):
            if not x % 15:
                yield "FizzBuzz\n"
            elif not x % 5:
                yield "Buzz\n"
            elif not x % 3:
                yield "Fizz\n"
            else:
                yield "%s\n" % x

How can I make this handler work asynchronously?

like image 446
David Eyk Avatar asked Jan 11 '12 00:01

David Eyk


2 Answers

Here's a basic version of what you are describing. To avoid blocking you can pass your generator to the IOLoop via a callback function. The trick here is since you are not using a process that does actual IO and so has no os level process/file handler to add to the IOLoop via add_handler, you can instead use a simple add_callback call and call it repeatedly from within the callback function to keep the function in the IOLoop callback queue until the generator has finished.

import tornado.httpserver
import tornado.ioloop
import tornado.web

class TextHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.generator = self.generate_text(1000)
        tornado.ioloop.IOLoop.instance().add_callback(self.loop)

    def loop(self):
        try:
            text = self.generator.next()
            self.write(text)
            tornado.ioloop.IOLoop.instance().add_callback(self.loop)
        except StopIteration:
            self.finish()

    def generate_text(self, n):
        for x in xrange(n):
            if not x % 15:
                yield "FizzBuzz\n"
            elif not x % 5:
                yield "Buzz\n"
            elif not x % 3:
                yield "Fizz\n"
            else:
                yield "%s\n" % x

application = tornado.web.Application([
    (r"/text/", TextHandler),
])

http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()
like image 108
philofinfinitejest Avatar answered Sep 16 '22 18:09

philofinfinitejest


It is also possible to use the new tornado's gen interface to async processes:

import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.gen

class TextHandler(tornado.web.RequestHandler):

    @tornado.web.asynchronous
    @tornado.gen.engine
    def get(self):

        def cb(it, callback):
            try:
                value = it.next()
            except StopIteration:
                value = None
            callback(value)

        it = self.generate_text(1000)
        while True:
            response = yield tornado.gen.Task(cb, it)
            if response:
                self.write(response)
            else:
                break
        self.finish()

    def generate_text(self, n):
        for x in xrange(n):
            if not x % 15:
                yield "FizzBuzz\n"
            elif not x % 5:
                yield "Buzz\n"
            elif not x % 3:
                yield "Fizz\n"
            else:
                yield "%s\n" % x

application = tornado.web.Application([
    (r"/text/", TextHandler),
])

http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()
like image 28
lbolla Avatar answered Sep 20 '22 18:09

lbolla