Как управлять зависимостями в дереве процессов?
В Elixir дерево процессов (process tree) реализовано с помощью иерархии Supervisor'ов и рабочих процессов (workers), где один процесс может контролировать жизненный цикл других. Управление зависимостями в этом дереве — это процесс настройки взаимосвязей между процессами: кто кого перезапускает, какие процессы важны, в каком порядке их запускать и как обеспечить отказоустойчивость. Основные инструменты для управления зависимостями — это Supervisor, стратегии перезапуска, вложенные супервизоры и соглашения OTP.
1. Supervisor и его роль в управлении зависимостями
Supervisor — это специальный процесс, задача которого следить за дочерними процессами (children) и перезапускать их при сбоях. Он описывает:
-
какие процессы запускаются,
-
в каком порядке,
-
что делать при сбое дочернего процесса.
Supervisor определяет иерархию, задаёт тип зависимости и обрабатывает сбои.
2. Типы зависимостей (restart strategies)
Разные стратегии перезапуска управляют тем, как ошибки в одном процессе влияют на другие. Это ключевой механизм управления зависимостями:
-
:one_for_one — при сбое дочернего процесса перезапускается только он.
-
:one_for_all — при сбое одного дочернего процесса все остальные перезапускаются.
-
:rest_for_one — при сбое дочернего процесса перезапускаются он и все запущенные после него.
-
:simple_one_for_one — устаревший вариант, заменён на :dynamic через DynamicSupervisor.
Эти стратегии позволяют выстраивать логические зависимости между процессами, определяя, какие компоненты взаимозависимы.
3. Использование Supervisor для описания зависимостей
children = \[
{MyApp.Repo, \[\]},
{MyApp.Cache, \[\]},
{MyApp.Worker, \[\]}
\]
opts = \[strategy: :rest_for_one, name: MyApp.Supervisor\]
Supervisor.start_link(children, opts)
В этом примере MyApp.Cache и MyApp.Worker зависят от MyApp.Repo. Если Repo падает, rest_for_one перезапустит все, кто был запущен позже.
4. Вложенные супервизоры (Nested Supervision)
Для более сложных и изолированных зависимостей используются вложенные супервизоры. Это отдельные Supervisor-поддеревья, которые инкапсулируют свою логику.
\# Main Supervisor
children = \[
{MyApp.DatabaseSupervisor, \[\]},
{MyApp.BusinessLogicSupervisor, \[\]}
\]
Supervisor.start_link(children, strategy: :one_for_one)
Каждый супервизор может иметь свою стратегию и набор зависимостей, изолируя ошибки.
5. Перезапуск и максимальные значения
Чтобы избежать бесконечных перезапусков при системных сбоях, Supervisor использует:
-
:max_restarts — сколько раз можно перезапустить за период,
-
:max_seconds — за сколько секунд считаются перезапуски.
Supervisor.start_link(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
Если превышено — сам Supervisor тоже падает, что может запустить перезапуск всего дерева.
6. Управление жизненным циклом: GenServer.terminate/2, Supervisor.which_children/1
Для управления и анализа процессов доступны:
-
Supervisor.which_children(SupervisorName) — возвращает список дочерних процессов и их статусы.
-
Supervisor.count_children/1 — количество активных, перезапущенных, ожидающих и т.п.
-
terminate/2 в GenServer — позволяет корректно закрыть ресурсы при завершении.
7. DynamicSupervisor — динамическое управление зависимостями
Если дочерние процессы появляются/исчезают во время выполнения, используют DynamicSupervisor.
{:ok, pid} = DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {MyWorker, arg})
Такой подход полезен, если количество зависимых процессов заранее неизвестно (например, сессии пользователей).
8. Модуль Application и точка входа в дерево
В Elixir приложение запускается через Application, в котором указывается корневой супервизор:
def start(\_type, \_args) do
children = \[
MyApp.Supervisor
\]
opts = \[strategy: :one_for_one, name: MyApp.RootSupervisor\]
Supervisor.start_link(children, opts)
end
Это точка входа в дерево зависимостей. Отсюда начинается запуск всей иерархии.
9. Fail Fast и изоляция
Важно проектировать дерево так, чтобы:
-
критические компоненты не зависели от нестабильных,
-
сбои в одних частях не затрагивали другие,
-
ошибки выявлялись быстро, а процессы "падали" как можно раньше — fail fast.
Это достигается правильной структурой супервизоров и выбором стратегий.
10. Пример сложной иерархии с зависимостями
defmodule MyApp.Application do
use Application
def start(\_type, \_args) do
children = \[
MyApp.DatabaseSupervisor, # должен запускаться первым
MyApp.CacheSupervisor, # зависит от базы
MyApp.API.Supervisor # зависит от Cache
\]
opts = \[strategy: :rest_for_one, name: MyApp.Supervisor\]
Supervisor.start_link(children, opts)
end
end
Если DatabaseSupervisor упадёт — rest_for_one перезапустит и Cache, и API.
Если упадёт только API.Supervisor — он будет перезапущен отдельно.
Иерархия процессов и контроль за их зависимостями — основа надёжных систем в Elixir. Управление ими основано на точной настройке стратегий перезапуска, правильной структуре дерева, вложенных супервизорах и разделении обязанностей.