Супервизор и приложение

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

Когда это происходит, ваша первая реакция может быть: «Добавлю-ка rescue для обработки ошибок». Но в Эликсире принято избегать «защитного» программирования с отловом ошибок. Напротив, «пускай падает». Если есть баг, который приводит к падению реестра, у нас нет повода волноваться, потому что мы сделаем супервизор, который запустит новую копию реестра.

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

Наш первый супервизор

Создание супервизора не слишком отличается от создания GenServer. Мы определим модуль с именем KV.Supervisor, который будет использовать поведение Supervisor внутри файла lib/kv/supervisor.ex:

defmodule KV.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  def init(:ok) do
    children = [
      KV.Registry
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Пока этот супервизор имеет единственного потомка: модуль KV.Registry. Когда мы определим несколько потомков, мы будем вызывать функцию Supervisor.init/2, передавая потомков и стратегию надзора за ними.

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

После старта супервизор пройдёт по списку потомков и выполнит функнцию child_spec/1 для каждого модуля. Вы слышали о функции child_spec/1 в главе «Агенты», когда вызывали функцию start_supervised(KV.Bucket) без указания модуля.

Функция child_spec/1 возвращает спецификацию потомка, которая объясняет, как запустить процесс, является ли он воркером или супервизором, является ли он временным или постоянным и т. д. Функция child_spec/1 автоматически определяется при использовании use Agent, use GenServer, use Supervisor и т. д. Давайте попробуем это на практике, выполнив команду iex -S mix:

iex(1)> KV.Registry.child_spec([])
%{
  id: KV.Registry,
  restart: :permanent,
  shutdown: 5000,
  start: {KV.Registry, :start_link, [[]]},
  type: :worker
}

Мы изучим другие детали по ходу этого руководства. Если вы хотите понять больше, загляните в раздел документации модуля Supervisor.

Когда супервизор получит все спецификации потомков, он запустит их один за другим, в порядке, в котором они определены, используя информацию по ключу :start в их спецификациях. В текущей спецификации он вызовет KV.Registry.start_link([]).

Пока start_link/1 всегда получает пустой список опций. Самое время изменить это.

Именование процессов

Наше приложение будет иметь много корзин, но только один реестр. Поэтому вместо передачи всюду идентификатора процесса реестра, мы можем дать ему имя и ссылаться на него по имени.

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

Давайте реализуем это. Немного изменим определение потомков из списка атомов в список кортежей:

  def init(:ok) do
    children = [
      {KV.Registry, name: KV.Registry}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

Разница в том, что вместо вызова функции KV.Registry.start_link([]) супервизор будет вызывать функцию KV.Registry.start_link([name: KV.Registry]). Если вы вернётесь к реализации функции KV.Registry.start_link/1, вы вспомните, что при этом будут переданы опции в GenServer:

  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

GenServer зарегистрирует процесс с переданным именем.

Давайте попробуем всё это в консоли iex -S mix:

iex> KV.Supervisor.start_link([])
{:ok, #PID<0.66.0>}

iex> KV.Registry.create(KV.Registry, "shopping")
:ok

iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.70.0>}

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

На практике редко запускают супервизор приложения вручную. Напротив, он стартует как часть колбэка в приложении.

Понимание приложений

Мы всё время работали внутри приложения. Каждый раз, когда мы изменяли файл и запускали mix compile, мы могли увидеть сообщение Generated kv app в выводе компиляции.

Мы можем найти сгенерированный файл .app в _build/dev/lib/kv/ebin/kv.app. Давайте посмотрим на его содержимое:

{application,kv,
             [{registered,[]},
              {description,"kv"},
              {applications,[kernel,stdlib,elixir,logger]},
              {vsn,"0.0.1"},
              {modules,['Elixir.KV','Elixir.KV.Bucket',
                        'Elixir.KV.Registry','Elixir.KV.Supervisor']}]}.

Этот файл содержит термы Эрланга (написан с использованием синтаксиса Эрланга). Даже если мы не знакомы с Эрлангом, достаточно просто предположить, что это определение приложения. Оно содержит версию нашего приложения (version), все определённые в нём модули, а также список приложений-зависимостей, например, kernel Эрланга, сам elixir и logger, который определён в списке :extra_applications внутри mix.exs.

Было бы очень неудобно обновлять этот файл вручную каждый раз, когда мы добавляем новый модуль в наше приложение. Поэтому Микс генерирует и поддерживает его актуальным за нас.

Мы также можем настроить генерируемый файл .app, изменив значения, возвращаемые application/0 внутри нашего файла проекта mix.exs. Мы скоро сделаем наши первые изменения.

Запуск приложений

Когда мы определяем файл .app, который является спецификацией приложения, мы можем запускать и останавливать приложение целиком. До сих пор мы не думали об этом по двум причинам:

  1. Микс автоматически запускал приложение за нас;

  2. Даже если Микс не запускал наше приложение за нас, оно ничего не делало при запуске.

В любом случае, давайте посмотрим, как Микс запускает приложение за нас. Откройте консоль проекта с помощью команды iex -S mix и попробуйте:

iex> Application.start(:kv)
{:error, {:already_started, :kv}}

Упс, оно уже запущено. Микс по умолчанию запускает всю иерархию приложений, объявленную в файле проекта mix.exs и то же происходит со всеми зависимостями, если они зависят от других приложений.

Мы можем с помощью опций запустить Микс с указанием не запускать приложение. Попробуйте ввести в консоль команду iex -S mix run --no-start:

iex> Application.start(:kv)
:ok

Мы можем остановить наше приложение :kv и, точно так же, приложение :logger, которое запустилось по умолчанию вместе с Эликсиром:

iex> Application.stop(:kv)
:ok

iex> Application.stop(:logger)
:ok

И давайте попробуем запустить наше приложение снова:

iex> Application.start(:kv)
{:error, {:not_started, :logger}}

Сейчас мы получаем ошибку, потому что какая-то из зависимостей :kv (в данном случае :logger) не запущена. Нам нужно также запустить каждое приложение вручную в правильном порядке или вызвать функцию Application.ensure_all_started как показано ниже:

iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}

Ничего удивительного не произошло, но мы увидели, как можем контроллировать наше приложение.

Запуск команды iex -S mix эквивалентен запуску команды iex -S mix run. Когда вам нужно передать больше опций в Микс при запуске IEx, важно написать именно iex -S mix run и затем передать любые опции, которые принимает команда run. Вы можете получить больше информации о run с помощью mix help run в вашей консоли.

Колбэк приложения

Мы всё время говорим о том, как приложения запускаются и останавливаются, а значит должен быть способ сделать что-нибудь полезное, когда приложение стартует. И, разумеется, он есть!

Мы можем определить колбэк приложения. Это функция, которая будет выполнена, когда приложение запустится. Функция должна возвращать результатом {:ok, pid}, где pid – идентификатор процесса в процессе-супервизоре.

Мы можем создать обратный вызов в два шага. Первый: откройте файл mix.exs и измените def application как показано ниже:

  def application do
    [
      extra_applications: [:logger],
      mod: {KV, []}
    ]
  end

Опция :mod определяет «модуль колбэка приложения», и аргументы, передаваемые туда при запуске приложения. Модулем колбэка приложения может быть любой модуль, который реализует поведение Application.

Т. к. теперь мы задали значение KV в качестве модуля колбэка, нам нужно изменить модуль KV, определённый в файле lib/kv.ex:

defmodule KV do
  use Application

  def start(_type, _args) do
    KV.Supervisor.start_link(name: KV.Supervisor)
  end
end

После добавления use Application, нужно определить пару функций, по аналогии с использованием Supervisor или GenServer. Сейчас нам достаточно определить только функцию start/2. Если бы мы хотели задать своё поведение для остановки приложения, мы могли бы определить функцию stop/1.

Давайте запустим консоль нашего проекта снова с помощью команды iex -S mix. Мы увидим, что процесс с именем KV.Registry уже запущен:

iex> KV.Registry.create(KV.Registry, "shopping")
:ok

iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.88.0>}

Как мы поняли, что это работает? Мы создали корзину и нашли её; это должно работать, так? Хорошо, вспомним, что функция KV.Registry.create/2 использует функцию GenServer.cast/2, и возвращает атом :ok независимо от того, нашло сообщение получателя или нет. В этот момент мы не знаем, запущены ли супервизор и сервер, и создана ли корзина. Однако, функция KV.Registry.lookup/2 использует функцию GenServer.call/3, и будет обязательно ждать ответа от сервера. Мы получаем положительный ответ, поэтому мы знаем, что всё запущено и работает.

Ради эксперимента попробуйте переделать функцию KV.Registry.create/2 на использованием функции GenServer.call/3 и тут же отключите колбэк приложения. Запустите код выше в консоли снова и вы увидите, что шаг с созданием сразу же будет падать.

Не забудьте вернуть код в нормальное состояние перед тем, как продолжить!

Проекты или приложения?

Для Микса есть разница между проектами и приложениями. Основываясь на содержимом файла mix.exs, мы могли бы сказать, что у нас Микс-проект, который определяет приложение :kv. Как мы увидим в дальнейших главах, существуют проекты, которые не определяют ни одного приложения.

Когда мы говорим «проект», вы должны думать о Миксе. Микс – это инструмент управления вашим проектом. Он знает, как компилировать ваш проект, как его тестировать и прочее. Он также знает, как компилировать и запускать приложение, относящееся к вашему проекту.

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

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

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