Spec-Zone.ru › Elixir 1.18

Исходный код Деревья управления и приложения

В предыдущей главе о GenServer мы реализовали KV.Registry для управления корзинами. На определенном этапе мы начали отслеживать корзины, чтобы иметь возможность действовать всякий раз, когда KV.Bucket выходил из строя. Хотя изменение было относительно небольшим, оно подняло вопрос, который часто задают разработчики Elixir: что происходит, когда что-то терпит неудачу?

До добавления мониторинга, если корзина выходила из строя, регистр навсегда указывал на корзину, которая больше не существовала. Если пользователь пытался читать или записывать в вышедшую из строя корзину, это приводило к ошибке. Любая попытка создания новой корзины с таким же именем просто возвращала PID вышедшей из строя корзины. Другими словами, запись в регистре для этой корзины навсегда оставалась в плохом состоянии. После добавления мониторинга регистр автоматически удалял запись для вышедшей из строя корзины. Попытка поиска вышедшей из строя корзины теперь (правильно) сообщает, что корзины не существует, и пользователь системы может успешно создать новую, если это необходимо.

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

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

Поэтому разработчик Elixir предпочитает «позволить ему выйти из строя» или «быстро выйти из строя». И одним из наиболее распространенных способов восстановления после сбоя является перезапуск той части системы, которая вышла из строя.

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

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

В этой главе мы узнаем, как применять эти концепции на практике, контролируя процесс KV.Registry. В конце концов, если что-то пойдет не так с регистром, весь регистр будет потерян, и ни одна корзина не сможет быть найдена! Чтобы решить эту проблему, мы определим модуль KV.Supervisor, который гарантирует, что наш KV.Registry работает в любой момент.

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

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

Супервайзер — это процесс, который контролирует другие процессы, которые мы называем дочерними процессами. Действие контроля процесса включает три отдельных обязанности. Первая — запустить дочерние процессы. После того, как дочерний процесс запущен, супервайзер может перезапустить дочерний процесс, либо потому, что он завершился аномально, либо потому, что было достигнуто определенное условие. Например, супервайзер может перезапустить всех дочерних процессов, если какой-либо из дочерних процессов завершился. Наконец, супервайзер также отвечает за завершение дочерних процессов при завершении работы системы. Подробное обсуждение см. в модуле Supervisor.

Создание супервайзера мало чем отличается от создания 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

  @impl true
  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 возвращает спецификацию дочернего элемента, которая описывает способ запуска процесса, является ли процесс рабочим или супервайзером, является ли процесс временным, переходным или постоянным и т. д. Функция child_spec/1 автоматически определена, когда мы use Agent, use GenServer, use Supervisor, и т. д. Давайте попробуем это в терминале с помощью iex -S mix:

iex> KV.Registry.child_spec([])
%{id: KV.Registry, start: {KV.Registry, :start_link, [[]]}}

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

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

Давайте запустим супервайзера:

iex> {:ok, sup} = KV.Supervisor.start_link([])
{:ok, #PID<0.148.0>}
iex> Supervisor.which_children(sup)
[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]

Пока что мы запустили супервайзера и перечислили его дочерние элементы. После запуска супервайзер также запустил все свои дочерние элементы.

Что произойдет, если мы намеренно выведем регистр, запущенный супервайзером, из строя? Давайте сделаем это, отправив ему неправильный ввод в call:

iex> [{_, registry, _, _}] = Supervisor.which_children(sup)
[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]
iex> GenServer.call(registry, :bad_input)
08:52:57.311 [error] GenServer #PID<0.150.0> terminating
** (FunctionClauseError) no function clause matching in KV.Registry.handle_call/3
iex> Supervisor.which_children(sup)
[{KV.Registry, #PID<0.157.0>, :worker, [KV.Registry]}]

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

В предыдущих главах мы всегда запускали процессы напрямую. Например, мы бы вызвали KV.Registry.start_link([]), который вернул бы {:ok, pid}, что позволило бы нам взаимодействовать с регистром через его pid. Теперь, когда процессы запускаются супервайзером, мы должны напрямую спросить супервайзера, каковы его дочерние элементы, и извлечь PID из возвращенного списка дочерних элементов. На практике каждый раз делать это было бы очень дорого. Чтобы решить эту проблему, мы часто даем имена процессам, что позволяет уникально идентифицировать их на одном компьютере из любого места в нашем коде.

Давайте узнаем, как это сделать.

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

Хотя наше приложение будет иметь множество корзин, у него будет только один регистр. Поэтому, когда мы запускаем регистр, мы хотим дать ему уникальное имя, чтобы мы могли обратиться к нему из любого места. Для этого мы передаем опцию :name в KV.Registry.start_link/1.

Давайте немного изменим определение наших дочерних элементов (в KV.Supervisor.init/1) на список кортежей вместо списка атомов:

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

С этим супервайзер теперь запустит KV.Registry, вызвав 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

который, в свою очередь, зарегистрирует процесс с заданным именем. Опция :name ожидает атом для локально именованных процессов (локально именованный означает, что он доступен на этом компьютере — есть и другие опции, которые мы здесь рассматривать не будем). Поскольку идентификаторы модулей являются атомами (попробуйте i(KV.Registry) в IEx), мы можем назвать процесс по модулю, который его реализует, при условии, что существует только один процесс для этого имени. Это помогает при отладке и интроспекции системы.

Давайте попробуем обновленный супервайзер в 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>}

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

На этом этапе вы можете задаться вопросом: нужно ли также локально называть процессы корзин? Помните, что корзины запускаются динамически на основе ввода пользователя. Поскольку локальные имена ДОЛЖНЫ быть атомами, нам нужно было бы динамически создавать атомы, что является плохой идеей, так как после определения атома его никогда не удаляют и не удаляют сборщик мусора. Это означает, что если мы динамически создаем атомы на основе ввода пользователя, в конечном итоге у нас закончится память (или, точнее, виртуальная машина выйдет из строя, потому что она накладывает жесткое ограничение на количество атомов). Именно из-за этого ограничения мы создали свой собственный регистр (или почему бы не использовать встроенный модуль Registry Elixir).

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

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

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

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

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

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

Приложение logger поставляется как часть Elixir. Мы указали, что наше приложение нуждается в нём, указав его в списке :extra_applications в mix.exs. Для получения дополнительной информации см. официальную документацию .

В двух словах, приложение состоит из всех модулей, определённых в файле .app, включая сам файл .app. Приложение, как правило, имеет только две директории: ebin, для артефактов Elixir, таких как файлы .beam и .app, и priv, содержащую любые другие необходимые артефакты или ресурсы вашего приложения.

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

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

Каждое приложение в нашей системе может быть запущено и остановлено. Правила запуска и остановки приложения также определены в файле .app. Когда мы вызываем iex -S mix, Mix компилирует наше приложение и затем запускает его.

Давайте посмотрим на практике. Запустите консоль с помощью iex -S mix и попробуйте:

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

Упс, оно уже запущено. Mix автоматически запускает текущее приложение и все его зависимости. Это также верно для mix test и многих других команд Mix.

Однако мы можем остановить наше приложение :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/1 следующим образом:

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

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

Обработчик приложения

Всякий раз, когда мы вызываем iex -S mix, Mix автоматически запускает наше приложение, вызвав Application.start(:kv). Но можем ли мы настроить то, что происходит при запуске нашего приложения? На самом деле, можем! Для этого мы определяем обработчик приложения.

Первый шаг — указать нашей декларации приложения (например, нашему файлу .app ), какой модуль будет реализовывать обработчик приложения. Давайте сделаем это, открыв mix.exs и изменив def application на следующее:

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

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

Чтобы реализовать поведение Application, мы должны use Application и определить функцию start/2. Цель функции start/2 — запустить супервайзера, который затем запустит любые дочерние сервисы или выполнит любой другой код, необходимый нашему приложению. Воспользуемся этой возможностью, чтобы запустить супервайзер KV.Supervisor, который мы реализовали ранее в этой главе.

Поскольку мы указали KV в качестве модуля-обработчика, давайте изменим модуль KV, определённый в lib/kv.ex, чтобы реализовать функцию start/2.

defmodule KV do
  use Application

  @impl true
  def start(_type, _args) do
    # Although we don't use the supervisor name below directly,
    # it can be useful when debugging or introspecting the system.
    KV.Supervisor.start_link(name: KV.Supervisor)
  end
end

Обратите внимание, что выполнением этого действия мы нарушаем шаблонный тестовый случай, который тестировал функцию hello в KV. Вы можете просто удалить этот тестовый случай.

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

Поведение Application также имеет обратный вызов stop/1, но на практике он используется редко. Более подробную информацию можно найти в документации.

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

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

Давайте подведём итоги. При вызове iex -S mix он автоматически запускает наше приложение, вызывая Application.start(:kv), которое затем вызывает обработчик приложения. Задача обработчика приложения — запустить дерево супервизирования. Сейчас наш супервайзер имеет единственного ребёнка с именем KV.Registry, запущенного под именем KV.Registry. Наш супервайзер может иметь других детей, и некоторые из них могут быть своими собственными супервайзерами с собственными детьми, что приводит к так называемым деревьям супервизирования.

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

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

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

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

Следующие шаги

Хотя в этой главе мы впервые реализовали супервайзер, это не было первым случаем его использования! В предыдущей главе, когда мы использовали start_supervised! для запуска реестра во время наших тестов, ExUnit запускал реестр под управлением супервайзера самого фреймворка ExUnit. Определяя свой собственный супервайзер, мы обеспечиваем больше структуры того, как мы инициализируем, останавливаем и контролируем процессы в наших приложениях, согласовывая наш производственный код и тесты с лучшими практиками.

Но мы ещё не закончили. Пока мы контролируем реестр, но наше приложение также запускает ведра. Поскольку ведра запускаются динамически, мы можем использовать специальный тип супервайзера, называемый DynamicSupervisor, который оптимизирован для обработки таких сценариев. Давайте изучим его дальше.

← Предыдущая страница Взаимодействие клиент-сервер с GenServer
Следующая страница → Управление динамическими дочерними элементами

Загрузить версию ePub

Создано с помощью ExDoc (v0.36.1) для язык программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/supervisor-and-application.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API