Что нового в Фениксе 1.4
Переведено в Докдоге – системе перевода технических текстов.
С начала нового года команда разработчиков Феникса семимильными шагами приближается к выпуску нового релиза 1.4 с новыми возможностями. Конечно, кое-какие вещи ещё нуждаются в доработке, но в ветке master
уже реализована поддержка HTTP2, сокращенное время компиляции, новый способ кодирования JSON и др. Сегодня мы посмотрим, над чем работала команда Феникса всё это время и что из этого вышло.
Поддержка HTTP2
Благодаря релизу Cowboy2 и трудам участника основной команды разработки Гэри Ренни, которому удалось осуществить интеграцию с Plug
, Феникс версии 1.4 будет поддерживать HTTP2. В новом релизе Феникса Cowboy2 пока будет доступен в виде дополнения, поскольку требуется ещё какое-то время на его доработку. Последующие релизы уже будут поставляться с HTTP2 по умолчанию. Если же вам важен HTTP2 сейчас, то подключить H2 очень просто: достаточно в конфигурациях эндпоинта заменить зависимость :cowboy
на ~> 2.0
и указать обработчик. HTTP2 предоставляет возможности server push и сокращенное время задержки. Более подробно читайте в обзорной статье об HTTP2 в Википедии.
Ускоренное время компиляции
Одно из основных преимуществ Феникса – это его скорость. И речь идёт не только о микросекундных значениях времени отклика в логах сервера. Скорость в продакшне – это лишь часть производительности. Мгновенное время отклика – прекрасный показатель, но если разработка представляет собой затянутый рутинный процесс, или тесты выполняются слишком медленно, то выходит, что победы в продакшне сильно понижают продуктивность разработки.
К счастью, Эликсир и Феникс оптимизируют весь процесс разработки. Неважно, какую задачу вы в данный момент выполняете – запуск тестов, разработка приложения или обработка запросов конечных пользователей - ваше приложение будет работать настолько быстро, насколько это возможно, используя все имеющиеся ядра и ресурсы компьютера.
Принимая это во внимание, команда разработчиков Феникса всегда стремится повысить производительность как в продакшне, так и на этапе разработки. Случалось, что в довольно больших приложениях компиляция выполнялась с каждым разом всё дольше и дольше. Оказалось, что маршрутизатор Феникса создавал длинные списки зависимостей времени компиляции во всей кодовой базе. Некоторые программисты во время работы над приложением сталкивались с длительной повторной компиляцией всех зависимых модулей.
Взгляд изнутри
Чтобы понять, почему маршрутизатор создаёт зависимости времени компиляции, необходимо подробнее рассмотреть, как работает Plug
и вытащить наружу немного метапрограммирования.
К примеру, определим плаг AuthenticateUser
, получающий параметры поиска пользователя текущей сессии.
defmodule MyAppWeb.AuthenticateUser do
def init(opts), do: Enum.into(opts, %{session_key: "user_id"})
def call(conn, %{session_key: key}) do
case conn.session[key] do
...
end
end
end
Для оптимизации поиска session_key
во время выполнения конвертируем список ключевых слов, передаваемый в plug
, в словарь, а также назначим параметры по умолчанию. Благодаря такому преобразованию и выставлению параметров в init
плаг будет выполнять действия во время компиляции, а не во время работы приложения. После этого каждый запрос будет передавать уже приведённые параметры, что поможет оптимизировать расход времени в дальнейшем.
Однако у данного подхода есть одно побочное действие: для его осуществления необходимо вызвать AuthenticateUser.init/1
на этапе компиляции, ведь именно здесь создаются зависимости времени компиляции. Можно увидеть, почему это происходит, посмотрев на код, генерируемый после вызова plug
. Если подключить плаг в маршрутизаторе вот таким образом:
pipeline :browser do
...
plug MyAppWeb.AuthenticateUser, session_key: "uid"
end
То сгенерируется следующий код:
case AuthenticateUser.call(conn, %{session_key: "uid"}) do
%Plug.Conn{halted: true} = conn ->
nil
conn
%Plug.Conn{} = conn ->
case ... do ... end
_ ->
raise("expected AuthenticateUser.call/2 to return a Plug.Conn")
end
Видите, условие case
содержит последний параметр %{session_key: "uid"}
? Все потому, что во время компиляции была вызвана функция AuthenticateUser.init/1
и сгенерировался код для запуска во время работы приложения. Это, конечно, положительно скажется на производительности, но, поскольку во время разработки проект постоянно компилируется снова и снова, хотелось бы обойтись без лишних движений на этом этапе в целях экономии времени.
Реализация решения
Объединив предыдущие идеи, получаем достаточно простое решение: генерируем код, оптимизируемый во время компиляции, в продакшне и тестовых окружениях, вызывая init
во время запуска приложения. Таким образом, сокращение количества зависимостей времени компиляции добавит немного работы времени исполнения. Однако в среде разработки ничего не изменится, поскольку приложение не нагружено.
Для реализации предложенного решения, был создан новый параметр init_mode
функции Plug.Builder.compile/3
, определяющий, где должна быть вызвана функция init/1
: :compile
- во время компиляции (задана по умолчанию), :runtime
- во время запуска. Чтобы Феникс поддерживал данные настройки, достаточно добавить следующую команду в mix
:
config :phoenix, :plug_init_mode, :runtime
После чего генерируемый код AuthenticateUser
в dev
будет выглядеть так:
case AuthenticateUser.call(conn, AuthenticateUser.init(session_key: "uid")) do
%Plug.Conn{halted: true} = conn ->
nil
conn
%Plug.Conn{} = conn ->
case ... do # further nested plug calls
_ ->
raise("expected AuthenticateUser.call/2 to return a Plug.Conn")
end
Каждый запрос к приложению вызывает AuthenticateUser.init/1
с вышеопределёнными параметрами, потому что приведение данных теперь выполняется на этапе запуска. В результате получаем ускоренную компиляцию одновременно с оптимизацией кода в продакшне.
Использование child_spec
в новых проектах на Эликсире 1.5+
Новый релиз Феникса также включает обновлённые и оптимизированные спецификации потомка Эликсира версии 1.5+.
В более ранних версиях Эликсира файл application.ex
Феникс-проектов содержал следующий код:
# lib/my_app/applicatin.ex
import Supervisor.Spec
children = [
supervisor(MyApp.Repo, []),
supervisor(MyApp.Web.Endpoint, []),
worker(MyApp.Worker, [opts]),
]
Supervisor.start_link(children, strategy: :one_for_one)
Новые проекты будут иметь такие спецификации:
children = [
Foo.Repo,
FooWeb.Endpoint,
{Foo.Worker, opts},
]
Supervisor.start_link(children, strategy: :one_for_one)
В новой версии Эликсира 1.5+ оптимизирован запуск и надзор над дочерними процессами путём внедрения спецификации потомка в модуль. Теперь разработчику не придётся думать, нужно ли запускать worker
или supervisor
, что позволит не только предотвратить баги, но и идти постепенно от более простой архитектуры к более сложной по необходимости. Например, начать с простого одиночного процесса-воркера и превратить его в целое дерево супервизоров. При этом код вызывающих процессов или функций, использующих процесс-воркер в своём дереве супервизоров, не будет нуждаться в правках. И это большой шаг вперёд в вопросах сопровождения и компонуемости приложений.
Явно заданные псевдонимы хелперов маршрутизатора
Также были убраны импорты MyAppWeb.Router.Helpers
из web.ex
в только что созданных приложениях, заменив их на явно заданные псевдонимы:
alias MyAppWeb.Router.Helpers, as: Routes
Таким образом, код в контроллерах и представлениях претерпит изменения, то есть вместо:
redirect(conn, to: article_path(conn, :index))
Предлагается вызывать функции маршрутизатора, используя явно заданные псевдонимы:
redirect(conn, to: Routes.article_path(conn, :index))
Это упростит жизнь при работе над новым проектом как новичкам, так и опытным разработчикам, ведь теперь намного проще понять, где определены функция маршрутизатора и где искать документацию в IEx
или ExDoc
. Также больше не придётся разбираться с цикличными ошибками компилятора при попытке импорта хелперов маршрутизатора в плаг-модуль, который также подключен внутри маршрутизатора.
Новый кодировщик JSON по умолчанию с библиотекой Jason
Следующий релиз Феникса также включает Jason
, новую JSON библиотеку, созданную Майклом Мускала из команды разработчиков Эликсира. Jason
- самый быстрый из существующих кодировщиков, написанный на Эликсире, в некоторых сценариях быстрее даже кодирующих библиотек на C. Он поддерживается участником команды разработчиков Эликсира, а значит, является идеальным выбором для тех, кто хочет получить максимум от своих приложений на Эликсире. Для подключения библиотеки Jason
только что созданные приложения будут содержать следующую конфигурацию в mix
:
config :phoenix, :json_library, Jason
В преддверии нового релиза команда разработчиков Феникса занимается отладкой всех вышеперечисленных обновлений. А мы ждём его скорейшего выхода!