ElixirSeniorSystem design

Что такое 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.

Sources

Related topics