Асинхронность — в django

1a4902276d55fd031b39631edb229d17

Здравствуйте, дорогие читатели Хабра и хаба про django. Да, эта статья о фреймворке для перфекционистов с дедлайнами и о том, как в нём не хватает асинхронности. По духу это больше похоже на Enhancement Proposal (менее формальный, чем он мог быть) или RFC, так что, если Вы любите подобные вещи, то Вам может быть интересно.

Вопросом добавления асинхронности сама Django Foundation тоже интересовалась. Дискуссии вылились в DEP-09, который описывает текущий примерный roadmap. Я даже неожиданно обнаружил, что мой этот пост ему не противоречит. Просто о нативной поддержке асинхронности там не очень много и написано. Это считается там последним этапом, до которого ещё нужно дойти. Напоминает мем про то, как рисовать сову: вначале рисуем два круга, потом дорисовываем остальное.

Но давайте, всё-таки, попробуем сделать django асинхронным. Точнее, django orm. Забыл сказать: я считаю django orm главным препятствием на пути django к асинхроннности. Это, как-никак, наибольшая по объёму часть. Потом, именно orm содержит тот набор допущений и характерных черт, которые делают django узнаваемым, как минимум.

Значит, orm. Драйвера базы данных нужны асинхронные, но такие, конечно, уже есть. Типичный код из orm выглядит схематично так:

def get_query(*query):
  compiler, sql, params = process_query(*query)
  rows = compiler.execute(sql, params)
  return objects_from(rows)

Как сделать его асинхронным? Правильно, передcompiler.execute добавить await, а перед функцией def get_query добавить async.

Кстати, не могу не упомянуть здесь об одной штуке, о которой сам недавно узнал. Есть способ не делать функцию compiler.execute асинхронной, но при этом использовать асинхронный драйвер базы данных. Это можно сделать, используя библиотеку greenlet — вот пример такого её использования. В общих чертах — мы переключаемся из синхронного кода в другой гринлет с помощью other_greenlet.switch(), и в конечном итоге, попадаем в асинхронную функцию. Такой подход, конечно, облегчает усилия по портированию кода в асинхронный. Кстати, именно по этому пути пошла sqlalchemy (так я, собственно, и узнал об этой возможности).

При всей заманчивости, мне это не очень нравится. Во-первых, это ломает отладку: greenlet содержит сишный код, и переключения между гринлетами непрозрачны для питона. Это, по сути, такой хак asyncio, на крайний случай. Ну, а orm — конечно, важная вещь (здесь немного иронии), но не настолько, чтобы подстраивать рантайм под неё. Но если Вам нравится этот вариант, проголосуйте за него в конце статьи!

Форк

Я предлагаю другой вариант, менее радикальный (с другой стороны — более). Не будем скупиться и выделим под асинхронную версию отдельный репозиторий (или, другими словами, форкнем django). Сделаем вначале API драйвера асинхронным (функции execute, fetchall, fetchone), потом постараемся использовать его вместо синхронного драйвера. Множество других функций придётся пометить как асинхронные и добавить await к их вызовам. В принципе, это и будут основные изменения, которые потребуются. django orm останется таким же с точностью до атрибутов каждого объекта — в большинстве случаев.

Конечно, будут исключения. Например, из-за того, что для запросов в базу нужно явно писать await, на ленивых запросах в базу, выполняющихся магически при доступе к атрибутам, наверно, придётся поставить крест. Что, не исключено, что к лучшему: или пишем await, и всегда выполняется запрос, или не пишем, в таком случае, объект уже должен находиться в кэше (например, в результате select_related или prefetch_related) — ясность!

Немного деталей по реализации: пусть форк, но он должен быть под другим именем: конфликт имён нам не нужен. Должна быть возможность использовать django и наш новый пакет в одном окружении без всяких проблем. То есть, понадобится какой-то скрипт (или просто PyCharm), который средствами статического анализа делает rename пакета django во что-то другое (например, teapot)

Есть распространённое мнение, что форк это плохо и в долгосрочной перспективе не поддерживаемо. Это спорный вопрос. Здесь хочу обратить внимание, что у нас специальный случай: новый синтаксис для асинхронных функций. То есть, изменения предсказуемы и прозрачны. Во-вторых, асинхронная версия полностью самодостаточна, на худой конец. Какая-бы то ни была совместимость с django ей не жизненно необходима. Ну, и моё мнение, что в самом форке нет ничего страшного, просто это мощное средство и требует некоторой ответственности.

Цели для первой версии

Первая версия должна быть озабочена портированием на асинхронные рельсы и ничем больше. Никаких новых фич. Портируем только orm, всё, что можно отсечь, отсекаем. То есть, например, формы и django админка — мимо. Несмотря на немаленький размер django, я уверен, что изменений сравнительно немного. Цели, которые нужно преследовать — это удобство мёрджей из джанго и переиспользование почти всех тестов из django почти без изменений.

Совместимость

Честно говоря, специально ничего делать для совместимости с синхронной версией я не планировал. Сама по себе асинхронная версия — это уже достижение. Но учитывая описанное выше, совместимость с синхронной версией останется сама собой. На уровне базы данных, я имею в виду: главным образом, соответствие между полями в моделях и схемой базы данных. И не использовать эту совместимость — неправильно, позволить её нарушить из-за какой-то ерунды — тоже. Поэтому, определённая совместимость будет, например, модели из django можно будет использовать в асинхронной версии. То есть, если пользователь хочет использовать модели как в синхронном, так и в асинхронном контексте, пусть объявляет их в models.py (и наследует от django.db.models.Model).

Перспективы развития

Если асинхронная версия django когда-то станет единственной, я не вижу в этом большой проблемы. Что касается самой django, у меня нет к ней серьёзных претензий или противоречий с ней. Разве что, хочется иметь поддержку zero-downtime migrations из коробки. Современные open-sorce реляционные базы это позволяют (проводить длительные операции, вроде индексирования, без блокировок) — значит, это должно быть дефолтом для современных orm. Тем более, что zero-downtime — и так обязательное требование для многих сервисов сегодня. Но эта тема за рамками данного текста.

DEP?

По сути — да. Потому что какая-то интеграция с django всё равно нужна, и будет. Но, вообще, enhancement proposals — это изменения в сам проект (django), а то, что может жить сторонним пакетом, или форки, DEP-ами не являются.

Но это DEP в том смысле, что он имеет целью только добавить поддержку асинхронности в django, никаких других изменений. Вообще, авторский элемент почти полностью отсутствует: наверно, каждый представляет, что должно получиться и что нужно делать.

© Habrahabr.ru