Используя простой генератор питона как совместную процедуру в обработчике асинхронного Tornado?

У меня есть функция генератора питона, которая дает куски текста. Я хотел бы написать метод get для подкласса tornado.web.RequestHandler, который будет перебирать генератор, записывая фрагменты в ответ, когда он идет.

Так как это Tornado, и поскольку генератор может занять несколько секунд для обработки, я подумал, что было бы неплохо сделать обработчик асинхронным, используя этот генератор как совместную процедуру и передав управление IOLoop после каждого куска, Тем не менее, я не могу сделать головы или хвосты, как это сделать.

Вот мой пример (блокирующий) код:

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

Как я могу заставить этот обработчик работать асинхронно?

Ответы

Ответ 1

Вот базовая версия того, что вы описываете. Чтобы избежать блокировки, вы можете передать генератор в IOLoop с помощью функции обратного вызова. Трюк здесь заключается в том, что вы не используете процесс, который делает фактический IO, и поэтому не имеет обработчика процесса/файла уровня os для добавления в IOLoop через add_handler, вместо этого вы можете использовать простой вызов add_callback и называть его повторно из в функции обратного вызова, чтобы сохранить функцию в очереди обратного вызова IOLoop до завершения генератора.

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()

Ответ 2

Также можно использовать новый интерфейс tornado gen для асинхронных процессов:

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()