Привет. Этот текст содержит предложения, как сделать джанго орм (а вместе с ним и сам джанго) асинхронным.

Как вы понимаете, после вышеуказанных обещаний, это не может не быть DEP (Django Enhancement Proposal). Да, это конкуррент DEP-09 (черновая версия). И хотя добавление асинхронности в джанго орм выходит за рамки DEP-09 (там это один из пунктов раздела "further individual projects") - я уверен, что предложенный ниже способ даже не рассматривался. В противном случае, DEP-09 выглядел бы совсем по-другому.

Итак: главная идея позаимствована из группы проектов sans.io. Всем рекомендую посмотреть про это https://www.youtube.com/watch?v=7cC3_jGwl_U Там предлагается не использовать код, содержащий I/O (ввод-вывод) в библиотеках, которые реализуют протоколы (вроде HTTP). Вынести это на более верхний уровень. Считать, что способ передачи данных неизвестен и может быть каким угодно, даже самым экзотическим. В результате, библиотеки sans.io можно использовать как в синхронном, так и в асинхронном контекстах.

То же самое можно применить к орм. Драйвер может быть каким угодно. Главное, что он умеет исполнять SQL и отдаёт строки с данными (tupl-ы, словари, не важно). Мы не будем привязываться к конкретному драйверу (вроде psycopg2 или asyncpg). Вместо этого построим наш апи абстрактного драйвера на колбэках. Выполнил SQL, получил строки - передай их в колбэк. Вроде такого:

sql, params = queryset.get_query()
# выполняем sql, как можем, потом
objects = queryset.process_rows(rows)

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

proof-of-concept

Итак, всё это выглядит очень смутно. Но, конечно, есть proof-of-concept. Осторожно, дальше примеры с реальным кодом (быстро закончится). Я подумал: если отвязать от драйвера только кверисеты, насколько это сложно? Чтобы сделать возможным запросы вида await queryset.

Итак: вот мой pull request https://github.com/pwtail/django/pull/1 Суть измененний в следующем: вместо итератора, который возвращает нам объекты, соответствующие какому-то запросу, у кверисета есть генератор: def _gen(). Как вы знаете, у генератора есть метод .send(): мы передаём какое-то значение, и получаем какое-то значение. В нашем случае это работает так: передай мне строки, который ты получил из базы данных по тому SQL и параметрам, которые я указал, и я верну тебе объекты. Всё немного сложнее, потому что вдобавок к основному queryset, есть ещё неявные, обусловленные, например, prefetch_related. Но, тем не менее, переделать апи кверисетов на генераторы оказалось не очень сложно. Результат: почти все тесты проходят, можно считать, что мы не сломали джангу. В случаях, когда тесты падают: это связано с тем, что иногда, например, вычисление SQL для кверисета бросает исключение raise EmptyQuerySet (при том, что пустой ответ не является исключительной ситуацией), и мне было лень его всюду отлавливать, иногда же мы получаем RuntimeError("generator raised StopIteration") вместо StopIteration (как выяснилось, StopIteration стали заворачивать в некоторых случаях в RuntimeError в одной из версий питона для нужд asyncio, подробности нужно уточнять).

Тем не менее, pull request доказывает, что подход работает: джанго не сломалась, а кверисеты можно использовать в асинхронном контекте (сам метод QuerySet.__await__ я не стал писать, но это потому, что я немного экспериментировал с этим раньше, и знаю, что это не представляет труда).

Ретроспективный взгляд

Такое название, потому что на рабочем проекте мы, как, возможно, и Вы, работаем по скраму, и ретроспектива у нас означает вполне конкретную вещь. В общем, использование генераторов - это было совсем не обязательно. Гораздо проще и безпроблемнее использовать обычный объект и обычные колбэки. Генератор - мощное средство, и если можно обойтись без него, то нужно это сделать. Тем не менее, подход работает.

To be continued. Что вы думаете?

Комментарии (0)