Как управлять зависимостями в дереве процессов?

В 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. Управление ими основано на точной настройке стратегий перезапуска, правильной структуре дерева, вложенных супервизорах и разделении обязанностей.