Статьи

Как изменить код Торнадо с помощью gen.engine

Иногда писать асинхронный код в стиле обратного вызова с помощью Tornado — это боль. Но настоящий вред приходит, когда вы хотите преобразовать свой асинхронный код в повторно используемые подпрограммы. Модуль Tornado gen упрощает рефакторинг, но сначала нужно изучить несколько трюков.

Например

Я буду использовать этот блог для иллюстрации. Я создал его с помощью Motor-Blog , тривиальной платформы для блогов поверх Motor , моего нового драйвера для Tornado и MongoDB .

Когда вы пришли сюда, Motor-Blog выполнил три или четыре запроса MongoDB для отображения этой страницы.

1 : найдите сообщение в блоге по этому URL и покажите вам этот контент.

2 и 3 : найдите следующий и предыдущий посты для отображения навигационных ссылок внизу.

Может быть 4 : Если список категорий слева изменился с момента последнего кэширования, получите список.

Давайте рассмотрим каждый запрос и посмотрим, как модуль tornado.gen облегчает жизнь.

Получение одного поста

В Tornado получение одного сообщения занимает немного больше работы, чем с кодом в стиле блокировки:

db = motor.MotorConnection().open_sync().my_blog_db

class PostHandler(tornado.web.RequestHandler):
    @tornado.asynchronous
    def get(self, slug):
        db.posts.find_one({'slug': slug}, callback=self._found_post)

    def _found_post(self, post, error):
        if error:
            raise tornado.web.HTTPError(500, str(error))
        elif not post:
            raise tornado.web.HTTPError(404)
        else:
            self.render('post.html', post=post)

Не так плохо. Но лучше ли с геном?

class PostHandler(tornado.web.RequestHandler):
    @tornado.asynchronous
    @gen.engine
    def get(self, slug):
        post, error = yield gen.Task(
            db.posts.find_one, {'slug': slug})

        if error:
            raise tornado.web.HTTPError(500, str(error))
        elif not post:
            raise tornado.web.HTTPError(404)
        else:
            self.render('post.html', post=post)

Чуть лучше. Оператор yield делает эту функцию генератором . gen.engine — блестящий хак, который запускает генератор до его завершения. Каждый раз, когда генератор выдает задание, gen.engine планирует возобновление работы генератора после завершения задачи. Прочитайте исходный код класса Runner для деталей, это волнующе. Или просто наслаждайтесь сваливанием всей своей логики в одну функцию снова, без определения каких-либо обратных вызовов.

Мотор включает в себя подкласс gen.Task под названием motor.Op. Он обрабатывает проверку и повышает исключение для вас, поэтому выше можно упростить:

@tornado.asynchronous
@gen.engine
def get(self, slug):
    post = yield motor.Op(
        db.posts.find_one, {'slug': slug})  
    if not post:
        raise tornado.web.HTTPError(404)
    else:
        self.render('post.html', post=post)

Тем не менее, нет огромных успехов. gen начинает светиться, когда вам нужно распараллелить некоторые задачи.

Получение следующего и предыдущего

Как только Motor-Blog находит текущее сообщение, он получает следующие и предыдущие сообщения. Поскольку эти два запроса независимы, мы можем сэкономить несколько миллисекунд, выполнив их параллельно. Как это выглядит с обратными вызовами?

@tornado.asynchronous
def get(self, slug):
    db.posts.find_one({'slug': slug}, callback=self._found_post)

def _found_post(self, post, error):
    if error:
        raise tornado.web.HTTPError(500, str(error))
    elif not post:
        raise tornado.web.HTTPError(404)
    else:
        _id = post['_id']
        self.post = post

        # Two queries in parallel
        db.posts.find_one({'_id': {'$lt': _id}},
            callback=self._found_prev)
        db.posts.find_one({'_id': {'$gt': _id}},
            callback=self._found_next)

def _found_prev(self, prev, error):
    if error:
        raise tornado.web.HTTPError(500, str(error))
    else:
        self.prev = prev
        if self.next:
            # Done
            self._render()

def _found_next(self, next, error):
    if error:
        raise tornado.web.HTTPError(500, str(error))
    else:
        self.next = next
        if self.prev:
            # Done
            self._render()

def _render(self)
    self.render('post.html',
        post=self.post, prev=self.prev, next=self.next)

Это отвратительно, и я хочу отказаться от Торнадо. Весь этот шаблон не может быть учтен. Поможет ли ген?

@tornado.asynchronous
@gen.engine
def get(self, slug):
    post, error = yield motor.Op(
        db.posts.find_one, {'slug': slug})
    if not post:
        raise tornado.web.HTTPError(404)
    else:
        prev, next = yield [
            motor.Op(db.posts.find_one, {'_id': {'$lt': _id}}),
            motor.Op(db.posts.find_one, {'_id': {'$gt': _id}})]

        self.render('post.html', post=post, prev=prev, next=next)

Теперь наша единственная функция get так же хороша, как и при блокировке кода. На самом деле, параллельная выборка намного проще, чем если бы вы использовали многопоточность вместо использования Tornado. Но как насчет выделения общей подпрограммы, которую могут совместно использовать обработчики запросов?

Выборка категорий

На каждой странице моего блога должен отображаться список категорий с левой стороны. Каждый обработчик запроса может просто включить это в свой метод get:

categories = yield motor.Op(
    db.categories.find().sort('name').to_list)

Но это ужасная техника. Вот как это сделать в подпрограмме с gen:

@gen.engine
def get_categories(db, callback):
    try:
        categories = yield motor.Op(
            db.categories.find().sort('name').to_list)
    except Exception, e:
        callback(None, e)
        return

    callback(categories, None)

Эта функция не обязательно должна быть частью обработчика запросов — она ​​стоит сама по себе в области видимости модуля. Чтобы вызвать его из обработчика запроса, выполните:

class PostHandler(tornado.web.RequestHandler):
    @tornado.asynchronous
    @gen.engine
    def get(self, slug):
        categories = yield motor.Op(get_categories)
        # ... get the current, previous, and next posts as usual, then ...
        self.render('post.html',
            post=post, prev=prev, next=next, categories=categories)

gen.engine запускает get до тех пор, пока не получит get_categories, затем отдельный движок запускает get_categories, пока не вызовет функцию обратного вызова, которая возобновляет get. Это почти как обычный вызов функции!

Это особенно приятно, потому что я хочу кешировать категории между просмотрами страниц. get_categories может быть обновлен очень просто, чтобы использовать кеш:

categories = None
@gen.engine
def get_categories(db, callback):
    global categories
    if not categories:
        try:
            categories = yield motor.Op(
                db.categories.find().sort('name').to_list)
        except Exception, e:
            callback(None, e)
            return

    callback(categories, None)

(Примечание для ботаников: я делаю кэш недействительным всякий раз, когда добавляется сообщение с ранее не замеченной категорией. Сигнал «новая категория» сохраняется в закрытой коллекции в MongoDB, к которой всегда привязываются все серверы Tornado. быть темой будущего поста.)

Вывод

Отличная документация модуля gen вкратце показывает, как метод, который выполняет несколько асинхронных вызовов, может быть упрощен с помощью gen.engine, но мощь действительно появляется, когда вам необходимо выделить общую подпрограмму. Сначала не очевидно, как это сделать, но есть только три шага:

1. Украсьте подпрограмму с помощью @ gen.engine.

2. Сделайте так, чтобы подпрограмма принимала аргумент обратного вызова (он должен называться обратным вызовом), которому подпрограмма передаст свои результаты после завершения.

3. Вызовите подпрограмму в функции, оформленной в виде движка, например:

result = yield gen.Task(subroutine)

результат содержит значение или значения, которые подпрограмма передала обратному вызову.

Если вы следуете соглашению Motor, согласно которому каждый обратный вызов принимает аргументы (результат, ошибка), то вы можете использовать motor.Op для обработки исключения:

result = yield motor.Op(subroutine)