Spec-Zone.ru › Elixir 1.17

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

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

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

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

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

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

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

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

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

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

Наш первый надзиратель

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

Создание надзирателя не сильно отличается от создания GenServer. Мы собираемся определить модуль с именем KV.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: попробуйте.

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

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

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

Мы работали внутри приложения всё это время. Каждый раз, когда мы меняли файл и запускали 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's 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.34.1) для языка программирования Elixir

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

Spec-Zone.ru

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