Что нового в Фениксе 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

В преддверии нового релиза команда разработчиков Феникса занимается отладкой всех вышеперечисленных обновлений. А мы ждём его скорейшего выхода!

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.