Рубрики
Без рубрики

Проблема с Async Async и Async Globals

Писал о том, почему Flask Async Spects и Python Elasticsearch Async Client не совместимы и предлагают некоторые потенциальные решения проблемы. Tagged с Python, OpenSource, Flask, Async.

Это Кросс-пост из моего блога. Если вам нравится мой контент, вы можете подписаться через Электронная почта или RSS Анкет

Начиная с v2.0 Флэста добавила асинхронные представления которые позволяют использовать Асинхронизация и ждет В рамках функции просмотра. Это позволяет вам использовать другие ASYNC API при создании веб -приложения с помощью колбы.

Если вы планируете использовать асинхронные представления Flask, есть внимание, чтобы узнать об использовании глобально определенных клиентов API или приспособлений, которые являются асинхронными.

Создание приложения Async Flask

В этом примере мы используем Async Elasticsearch Python Client как наш глобальный матч. Мы инициализируем простое приложение Flask с одним асинхронным представлением, который делает запрос с Async Elasticsearch Client:

# app.py
from flask import Flask, jsonify
from elasticsearch import AsyncElasticsearch

app = Flask(__name__)

# Create the AsyncElasticsearch instance in the global scope.
es = AsyncElasticsearch(
    "https://localhost:9200",
    api_key="..."
)

@app.route("/", methods=["GET"])
async def async_view():
    return jsonify(**(await es.info()))

Бег с Онломщик через $ umericorn App: App а затем посещение приложения через http://localhost: 8000 Анкет После первого запроса все выглядит нормально:

{
  "cluster_name": "d31d9d6abb334a398210484d7ac8567b",
  "cluster_uuid": "K5uyniiMT9u2grNBmsSt_Q",
  "name": "instance-0000000001",
  "tagline": "You Know, for Search",
  "version": {
    "build_date": "2021-04-20T20:56:39.040728659Z",
    "build_flavor": "default",
    "build_hash": "3186837139b9c6b6d23c3200870651f10d3343b7",
    "build_snapshot": false,
    "build_type": "docker",
    "lucene_version": "8.8.0",
    "minimum_index_compatibility_version": "6.0.0-beta1",
    "minimum_wire_compatibility_version": "6.8.0",
    "number": "7.13.1"
  }
}

Однако, когда вы обновляете страницу, чтобы отправить второй запрос, вы получите InternalError и следующий след:

Traceback (most recent call last):
  ...
  File "/app/app.py", line 13, in async_route
    return jsonify(**(await es.info()))
  File "/app/venv/lib/...", line 288, in info
    return await self.transport.perform_request(
  File "/app/venv/lib/...", line 327, in perform_request
    raise e
  File "/app/venv/lib/...", line 296, in perform_request
    status, headers, data = await connection.perform_request(
  File "/app/venv/lib/...", line 312, in perform_request
    raise ConnectionError("N/A", str(e), e)

ConnectionError(Event loop is closed) caused by:
  RuntimeError(Event loop is closed)

Почему это происходит?

В сообщении об ошибке упоминается, что цикл события закрыт, а? Чтобы понять, почему это происходит, вам нужно знать, как Asyncelasticsearch реализован и как работают асинхронные взгляды в колбе.

Нет глобальных циклов событий

Async Code опирается на то, что называется цикла событий. Итак, любой код, использующий Асинхронизация или ждет Не могу выполнить без цикла событий, который «работает». К сожалению, когда вы запускаете Python (т. Е. Глобальный объем).

Вот почему вы не можете иметь код, который выглядит так:

async def f():
    print("I'm async!")

# Can't do this!
await f()

Вместо этого вы должны использовать asyncio.run () и обычно Асинхронизация Основная/входная функция для использования Ждите вот так:

import asyncio

async def f():
    print("I'm async!")

async def main():
    await f()

# asyncio starts an event loop here:
asyncio.run(main())

(Есть исключение из этого через python -m asyncio / Ipython , но на самом деле это запускает реплику после начала цикла событий)

Поэтому, если вам нужен цикл событий для запуска любого асинхрового кода, как вы можете определить Asyncelasticsearch экземпляр в глобальной области?

Как Asynclasticsearch позволяет глобальные определения

Магия глобальных определений для Asyncelasticsearch задерживает полную инициализацию вызова asyncio.get_running_loop () , Создание aiohttp. Сессия , нюхание и т. Д. До тех пор, пока мы не получили наш первый Асинхронизация вызов. Однажды Асинхронизация Звонок сделан, мы можем почти гарантировать, что есть цикл бега, потому что, если не было никакого цикла, которое в любом случае не сработает.

Особенно это отлично подходит для асинхронных программ, поскольку обычно используется один цикл событий на протяжении всего выполнения программы и означает, что вы можете создать свой Asyncelasticsearch экземпляр в глобальном объеме, как пользователи создают свои синхронные Elasticsearch Клиент в глобальном масштабе.

Использование нескольких петлей событий сложно и, вероятно, нарушит многие другие библиотеки, такие как aiohttp В процессе NO (?) Преимущество, поэтому мы не поддерживаем эту конфигурацию. Теперь, как это разрывается при использовании с новыми асинхронными представлениями Фласки?

Новый цикл событий по асинхронному запросу

Простое объяснение состоит в том, что Flask использует WSGI для обслуживания HTTP -запросов и ответов, которые не поддерживают асинхронный ввод -вывод. Асинхронный код требует выполнения цикла, поэтому Flask необходимо получить цикл с запущенным событием откуда -то, чтобы выполнить асинхронное представление.

Для этого Flask создаст новый цикл событий и начнет запустить представление в этом новом цикле событий для каждого выполнения асинхронного представления. Это означает, что все асинхронные и ожидающие вызовы в представлении увидят тот же цикл событий, но любой другой запрос до или после этого представления увидит другой цикл событий.

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

Исправление проблемы

Проблема не в Flask или клиенте Python Elasticsearch, проблема в том, что несовместимость между WSGI и асинхронные глобалы. Есть несколько решений, оба из которых связаны с Интерфейс шлюза Async Server (ASGI) , Двоюродный брат WSGI со вкусом асинк, который был разработан с учетом асинхронных программ.

Используйте фреймворк и сервер ASGI

Один из способов полностью избежать проблемы с WSGI – просто использовать нативную структуру веб -приложения ASGI. Есть несколько популярных и широко используемых фреймворков ASGI, которые вы можете выбрать:

Если вы ищете опыт, который очень похож на колбу, вы можете использовать Кварта который вдохновлен Флакой. У кварта даже есть Руководство о том, как мигрировать из приложения колбы на использование кварта ! Собственная документация Flask для асинхронных представлений на самом деле Рекомендуется использовать кварта В некоторых случаях из -за удара производительности с использованием нового цикла событий для запроса.

Если вы хотите узнать что -то новое, вы можете проверить FASTAPI который включает в себя кучу встроенных функций для документирования API, строгих модельных объявлений и проверки данных.

При разработке приложения ASGI, что нужно иметь в виду, вам нужен ASGI-совместимый сервер Анкет Общий выбор включает Uvicorn , Гиперкорн и Дафна Анкет Другой вариант – использовать Онломщик с Uvicorn Workers Анкет

Все упомянутые выше варианты функционируют довольно аналогично, так что выберите тот, который вам нравится. Мой личный выбор исторически был стреляющим из работников Uvicorn из -за того, насколько широко используется и зрелый стрелобник относится к тому, насколько новыми являются другие библиотеки.

Вы можете сделать это так:

$ gunicorn app:app -k uvicorn.workers.UvicornWorker

Используйте wsgitoasgi из Asgiref

Если вы действительно любите Flask и хотите продолжать использовать ее, вы также можете использовать Asgiref Пакет обеспечивает простую обертку под названием Wsgitoasgi Это преобразует приложение WSGI в приложение ASGI.

from flask import Flask, jsonify
from elasticsearch import AsyncElasticsearch

# Same definition as above...
wsgi_app = Flask(__name__)
es = AsyncElasticsearch(
    "https://localhost:9200",
    api_key="..."
)

@wsgi_app.route("/", methods=["GET"])
async def async_view():
    return jsonify(**(await es.info()))

# Convert the WSGI application to ASGI
from asgiref.wsgi import WsgiToAsgi

asgi_app = WsgiToAsgi(wsgi_app)

В этом примере мы преобразуем приложение WSGI wsgi_app в приложение ASGI asgi_app Это означает, что когда мы запускаем приложение, для каждого запроса будет использоваться один цикл событий, а не для нового цикла событий по запросу.

Этот подход по-прежнему потребует, чтобы вы использовали ASGI-совместимый сервер.

Оригинал: “https://dev.to/sethmlarson/the-problem-with-flask-async-views-and-async-globals-pl”