Skip to content

vera-l/frontik-patterns

Repository files navigation

Паттерны разработки для frontik-приложений

Запуск тестового проекта ^

python3 run.py

Затем нужно открыть в браузере http://0.0.0.0:8000, далее переходить по ссылкам. В дебаг-режиме будет доступна временнáя диаграмма, где темно-зеленым цветом обозначены асинхронные http-походы в соответствии со временем выполнения.

frontik_patterns screenshot на скришоте дебаг-страница с 4-мя последовательными запросами одинаковой длительности

Добавление своих примеров ^

Проект содержит страницу для выполнения примеров запросов 0.0.0.0:8000/example_request?duration=1, где duration - продолжительность запроса в секундах. Хендлер example_request.py содержит неблокирующий вызов asyncio.sleep().

http-клиент фронтика ^

При использовании современных http-клиентов либо библиотек для работы с базами обычно подобные методы возвращают корутины. При создании корутины-объекта из корутины-функции (вызов coro()) задача не ставится на выполнение сама - для этого мы должны заавейтить ее либо создать таску.

Давайте разберемся, что же представляют собой методы get_url, post_url, put_url и delete_url из фронтика.

  1. Под капотом они пока еще реализованы на старых торнадовских коллбеках, и при вызове любого метода http-клиента запрос сразу же ставится на выполнение. Это аналог современных тасок.
  2. Метод возвращает asyncio.Future, поэтому мы можем await'ить эту фьючу из AwaitablePageHandler и yield'ить ее же из PageHandler, дожидаясь результатов запроса для последующей обработки. Фьюча является тем костылем по сути, который связывает коллбеки и корутины, нативные и торнадовские. Теория
future = handler.get_url(
    some_host,
    '/path/to/resource',
    data=data,
    headers=headers,
)
result = await future

В коде старых проектов вместо yield либо await используется аргумент callback (чтобы обработать результат запроса), который не рекомендуется в современном коде.

def cb(xml, response):
    if xml is None or response.error:
        raise HTTPError(response.code)
    handler.doc.put(xml)

handler.get_url(
    some_host,
    '/path/to/resource',
    data=data,
    headers=headers,
    callback=cb,
)

Если при вызове http-метода не указывать флаг waited=False, то фьюча добавится в handler.finish_group и гарантированно выполнится до отдачи ответа.

Паттерны ^

Нативные корутины async def ^

Доступны только внутри AwaitablePageHandler. Все его http-методы должны быть нативными корутинами. Внутри этих методов доступны все возможности современного асинхронного питона - походы в базы, кеши, очереди и многое другое. Это самый лучший вариант, хорошо оптимизированный на уровне интерпретатора.

Последовательное выполнение ^

[ debug page, code ]

Делается ключевым словом await - код ниже этой инструкции продолжит выполняться только после завершения работы запроса/корутины.

await self.get_url(
    some_host,
    '/request1',
)
await self.get_url(
    some_host,
    '/request2',
)

Можно авейтить как сам запрос self.get_url(), так и корутину, делающую запрос и как-то обрабатывающую его результат.

[ debug page, code ]

async def get_result(handler):
    result = await handler.get_url(
        some_host,
        '/request2',
    )
    if result.failed:
        return ''
  
    return result.data['key']

key = await get_result(self)

Параллельное выполнениее ^

[ debug page, code ]

Делается при помощи asyncio.gather(), который принимает аргументы через запятую.

result1, result2 = await asyncio.gather(
    self.get_url(
        some_host,
        '/request1',
    ), 
    self.get_url(
        some_host,
        '/request2',
    )
)

Если нужно в метод передать массив фьюч/корутин, то его надо раскрыть. В переменной results будет содержаться массив с результатами, по количеству фьюч.

futures = [future1, future2]
results = asyncio.gather(*futures)

Можно использовать в asyncio.gather() как http-методы, которые возвращают фьючи, так и корутины. Иногда удобно использовать словарь, сопоставляя ключ и фьючи, как это было в торнадо. Для этого дабавлен метод gather_dict.

[ debug page, code ]

from frontik.util import gather_dict

coro_dict = {
    'result1': self.get_url(
      some_host,
      '/request1',
    ),
    'result2': self.get_url(
      some_host,
      '/request2',
    ),
}

result = await gather_dict(coro_dict=coro_dict)
key = result['result1'].data['key']

Независимая таска ^

Выполняется "параллельно" основному коду, не блокирует его, если ее явно не авейтить). Будет добавляться в self.finish_group и авейтиться с ней перед отдачей ответа. Может быть полезна в сложном коде, чтобы вынести какие-то независимые задачи из основного кода, а также если надо вначале запустить задачу, а заавейтить где-то позже в коде. По сути, воспроизводит поведение [http-поход+обработка в коллбеке] в старом коде. Задачи должны быть действительно независимыми, чтобы не повторять ситуации гонки в коллбечном коде. Реализована за счет asyncio.create_task(coro()).

! Таску можно создать только из корутины, но из http-метода, который возвращает фьючу - нет !

[ debug page, code ]

async def put_result(handler):
    result = await handler.get_url(
        some_host,
        '/request2',
    )
    if result.failed:
        return
  
    handler.doc.put(result.data)
    return result.data
  
task = self.run_task(put_result(self))
# .... заавейтить можно позже; в любом случае заавейтится в finish_group
result = await task

Корутины торнадо @gen.coroutine [deprecated] ^

Все http-методы обычного PageHandler являются корутинами торнадо, так как они декорируются @gen.coroutine в коде фронтика. Начиная с 5 версии, в торнадо под капотом стали использоваться нативные возможности асинхронного питона, в первую очередь event loop и Future из asyncio. По сути, корутины торнадо являются костылями к ним, что приводит к большому стеку вызовов и снижению производительности по сравнению с нативными корутинами.

Последовательное выполнение ^

[ debug page, code ]

Делается ключевым словом yield - код ниже этой инструкции продолжит выполняться только после завершения работы запроса/корутины.

yield self.get_url(
    some_host,
    '/request1',
)
yield self.get_url(
    some_host,
    '/request2',
)

Можно yield-ить как сам запрос self.get_url(), так и корутину, делающую запрос и как-то обрабатывающую его результат.

[ debug page, code ]

@gen.coroutine
def get_result(handler):
    result = yield handler.get_url(
        some_host,
        '/request2',
    )
    if result.failed:
        return ''
  
    return result.data['key']

key = yield get_result(self)

Параллельное выполнениее ^

[ debug page, code ]

Делается при помощи все того же yield, передавая ему массив корутин. Данные для него под капотом оборачиваются в gen.multi.

result1, result2 = yield [
    self.get_url(
        some_host,
        '/request1',
    ), 
    self.get_url(
        some_host,
        '/request2',
    )
]

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

[ debug page, code ]

results = yield {
    'result1': self.get_url(
      some_host,
      '/request1',
    ),
    'result2': self.get_url(
      some_host,
      '/request2',
    ),
}

key = result['result1'].data['key']

Независимая таска ^

Выполняется "параллельно" основному коду, не блокирует его, если ее явно не авейтить). Будет добавляться в self.finish_group и авейтиться с ней перед отдачей ответа. Может быть полезна в сложном коде, чтобы вынести какие-то независимые задачи из основного кода, а также если надо вначале запустить задачу, а за-yield'ить где-то позже в коде. По сути, воспроизводит поведение [http-поход+обработка в коллбеке] в старом коде.

! Таску нужно создавать только из корутины, так как сам по себе http-метод по сути является таской, которая запускается сразу же, и будет заавейчена в finish_group. Если в корутине есть запрос, он будет гарантированно выполнен до отдачи ответа, а вот обработка результата - не факт что успеет, поэтому такую корутину нужно вручную добавить в finish_group.

[ debug page, code ]

@gen.coroutine
def put_result(handler):
    result = yield handler.get_url(
        some_host,
        '/request2',
    )
    if result.failed:
        return
  
    handler.doc.put(result.data)
    return result.data

fut = get_result(self, 1.1)
self.finish_group.add_future(fut)
# .... за-yield'ить можно позже; в любом случае за-yield'ится в finish_group
result = yield task

Коллбеки торнадо [deprecated] ^

Коллбеки в версии 5.1.1 были задепрекейчены, а в 6 выпилены - чтобы полностью соответствовать требованиям современной асинхронной разработки на питоне - "в пользовательском коде должны быть только корутины и таски, а коллбеки и фьючи использоваться только для низкоуровневых задач". Для выполнения нескольких параллельных запросов и затем общего результирующего коллбека во фронтик-приложениях использовалась AsyncGroup

Последовательное выполнение ^

[ debug page, code ]

В коллбек предыдущего запроса добавляется следующий запрос со своим коллбеком - и так далее.

def cb(data, response):
    self.get_url(
      '0.0.0.0:8000',
      '/example_request',
      data={'duration': 0.5},
      callback=cb2,
    )

self.get_url(
  '0.0.0.0:8000',
  '/example_request',
  data={'duration': 0.5},
  callback=cb,
)

Параллельное выполнениее ^

[ debug page, code ]

Делается при помощи AsyncGroup, которой передается финальный коллбек для данной группы, а каждому из вызовов http-методов - коллбек, обернутый в метод add этой AsyncGroup'ы.

results = []

def finish_cb():
    print(results)

def cb(json, response):
    results.append(json)

async_group = AsyncGroup(finish_cb)

self.get_url(
    some_host,
    '/request1',
    callback=async_group.add(cb),
)
self.get_url(
  some_host,
    '/request2',
    callback=async_group.add(cb),
)

Независимая таска ^

Сам вызов http-метода с коллбеком или без него по сути является независимой таской, так как запускается сразу же, а его фьюча добавляется в finish_group.

! Иногда в старом коде (в новом это не рекомендуется) можно встретить вызов http-метода с коллбеком из торнадовской корутины. В этом случае задача, как обычно, запустится в месте вызова, когда придет время выполнения этой корутины, и заавейтится в finish_group.

[ debug page, code ]

def cb(json, response):
    results.append(json)

self.get_url(
    some_host,
    '/request1',
    callback=cb,
)

About

A frontik async tasks patterns demo

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages