Что такое Supervisor в Elixir и какие стратегии надзора (supervision strategies) существуют?
Supervisor — специальный OTP-процесс, который мониторит дочерние процессы и перезапускает их при падении. Стратегии: :one_for_one, :one_for_all, :rest_for_one. DynamicSupervisor — для динамически создаваемых дочерних процессов.
Supervisor в Elixir: модуль и стратегии
Supervisor — это процесс OTP, реализующий поведение Supervisor behaviour. Его единственная задача — наблюдать за дочерними процессами и перезапускать их согласно заданной политике. Supervisor устанавливает links к дочерним процессам и перехватывает EXIT-сигналы через Process.flag(:trap_exit, true).
Объявление Supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
MyApp.Repo,
MyApp.Cache,
{MyApp.Worker, name: :my_worker}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Стратегии надзора
:one_for_one
При падении дочернего процесса перезапускается только он. Подходит для независимых процессов, которые не влияют друг на друга.
Supervisor.init(children, strategy: :one_for_one)
# A падает -> перезапускается только A
# B и C продолжают работать
:one_for_all
При падении любого дочернего процесса останавливаются и перезапускаются все остальные. Нужно, когда все процессы работают вместе и не могут функционировать независимо.
Supervisor.init(children, strategy: :one_for_all)
# A падает -> останавливаются B и C, затем перезапускаются A, B, C
:rest_for_one
При падении процесса перезапускаются он и все процессы, объявленные после него в списке. Используется для pipeline, где каждый следующий процесс зависит от предыдущего.
children = [ProducerA, ConsumerB, LoggerC]
Supervisor.init(children, strategy: :rest_for_one)
# ConsumerB падает -> останавливается LoggerC,
# затем перезапускаются ConsumerB и LoggerC
# ProducerA продолжает работать
Child Spec: настройка перезапуска
children = [
# Сокращённая форма — модуль реализует child_spec/1
MyApp.Cache,
# Явный child spec с параметрами
%{
id: MyApp.Worker,
start: {MyApp.Worker, :start_link, [[]]},
restart: :permanent, # :permanent | :transient | :temporary
shutdown: 5000, # время (мс) на graceful stop
type: :worker # :worker | :supervisor
},
# Через Supervisor.child_spec/2 для переопределения
Supervisor.child_spec({MyApp.Job, []}, id: :job_1, restart: :transient)
]
DynamicSupervisor
Когда дочерние процессы создаются динамически во время работы приложения:
defmodule MyApp.RoomSupervisor do
use DynamicSupervisor
def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_children: 1000 # ограничение на количество дочерних процессов
)
end
def start_room(room_id) do
spec = {MyApp.Room, room_id: room_id}
DynamicSupervisor.start_child(__MODULE__, spec)
end
def stop_room(room_id) do
case Registry.lookup(MyApp.Registry, room_id) do
[{pid, _}] -> DynamicSupervisor.terminate_child(__MODULE__, pid)
[] -> :ok
end
end
end
Мониторинг и управление
# Список дочерних процессов
Supervisor.which_children(MyApp.Supervisor)
# Статистика
Supervisor.count_children(MyApp.Supervisor)
# => %{active: 3, specs: 3, supervisors: 1, workers: 2}
# Принудительный рестарт дочернего процесса
Supervisor.terminate_child(MyApp.Supervisor, MyApp.Cache)
Supervisor.restart_child(MyApp.Supervisor, MyApp.Cache)
# Добавление нового дочернего процесса к работающему supervisor
Supervisor.start_child(MyApp.Supervisor, {MyApp.NewWorker, []})
Подводные камни
- use Supervisor генерирует child_spec/1 — если определить его вручную неправильно, supervisor не сможет корректно перезапустить модуль.
- Стратегия :one_for_all на корневом supervisor означает, что любой сбой рестартует весь Endpoint и разрывает все WebSocket-сессии пользователей.
- Параметр shutdown: :infinity для воркеров может заблокировать остановку приложения навсегда, если процесс завис — всегда устанавливайте конкретный таймаут.
- DynamicSupervisor без max_children позволяет создавать неограниченное количество процессов — утечка при неправильном управлении жизненным циклом.
- Supervisor.restart_child/2 работает только для :permanent и :transient детей; для :temporary вернёт {:error, :undefined}.
- Вызов Supervisor.start_child/2 не является идемпотентным — повторный вызов с тем же id вернёт {:error, {:already_started, pid}} или {:error, :already_present}.
- Мониторинг через :telemetry не встроен в Supervisor — настройте :telemetry_metrics или PromEx для отслеживания restart rate по каждому дочернему процессу.
Common mistakes
- Сводить supervisor strategies к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Elixir компилируется в BEAM bytecode и наследует процессы, message passing, supervision и hot-code friendly модель Erlang VM.
- Не отделять validation, authorization, transaction boundary и business logic.
- Не обсуждать idempotency, retries, shutdown и observability.
What the interviewer is testing
- Объясняет supervisor strategies через конкретную точку lifecycle в Elixir.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
- Связывает решение с метриками, backpressure, retry policy и graceful shutdown.