Spec-Zone.ru › Elixir 1.16

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

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

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

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

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

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

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

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

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

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

Наш первый Supervisor

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

Создание 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

Наш Supervisor имеет пока единственного ребенка: KV.Registry. После определения списка детей мы вызываем Supervisor.init/2, передавая детей и стратегию управления.

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

После запуска Supervisor он перебирает список детей и вызывает функцию child_spec/1 для каждого модуля.

Функция child_spec/1 возвращает спецификацию ребёнка, которая описывает, как запустить процесс, является ли процесс рабочим или Supervisor, является ли процесс временным, временным или постоянным и так далее. Функция 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.

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

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

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]}]

На данный момент мы запустили Supervisor и вывели список его детей. После запуска Supervisor он также запустил всех своих детей.

Что произойдёт, если мы намеренно выведем из строя реестр, запущенный Supervisor? Давайте сделаем это, отправив ему неверный ввод в 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]}]

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

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

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

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

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

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

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

С этим Supervisor теперь запустит 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), мы можем назвать процесс по имени модуля, который его реализует, при условии, что для этого имени существует только один процесс. Это помогает при отладке и интроспекции системы.

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

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

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

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

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

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

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

Spec-Zone.ru

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