Что произойдёт, если процесс "падает"?

В Elixir, как и в Erlang, падение (crash) процесса — это не ошибка, а нормальное и ожидаемое поведение в рамках архитектуры «Let it crash». Если процесс «падает» (т.е. завершается из-за ошибки, необработанного исключения или выхода), последствия зависят от того, как он был запущен, есть ли у него родительский процесс, супервизор, ссылки на другие процессы, и как обрабатывается его завершение.

1. Что значит, что процесс «упал»

Процесс падает, если:

  • Внутри него происходит исключение, и оно не перехвачено.

  • Он получает невалидное сообщение.

  • Он вызывает exit/1 с ненулевым статусом.

  • Он вызывает raise, throw, error.

Пример:

spawn(fn -> raise "Ошибка!" end)

Этот процесс немедленно завершится с ошибкой.

2. Независимый процесс: spawn

Если процесс создан с помощью spawn/1, и он падает — никакие другие процессы об этом не узнают:

pid = spawn(fn -> raise "Ошибка!" end)
\# процесс завершится, но никто не уведомлён

Такой процесс считается несвязанным и не влияет на другие процессы.

3. Связанные процессы: spawn_link

Если процесс создан с spawn_link/1, он связывается с родительским процессом. В случае падения одного из них, второй также получает сигнал выхода и тоже завершится, если не перехватит этот сигнал:

spawn_link(fn -> raise "Ошибка!" end)
\# вызовет завершение и текущего процесса тоже

Можно обработать через Process.flag(:trap_exit, true).

4. Перехват выхода процесса: trap_exit

Чтобы сделать процесс устойчивым к падениям дочерних процессов, можно включить режим перехвата:

Process.flag(:trap_exit, true)
spawn_link(fn -> raise "что-то пошло не так" end)
receive do
{:EXIT, \_pid, reason} -> IO.inspect(reason)
end

Такой приём используется для создания надёжных супервизоров и логики восстановления.

5. Использование Task и обработка ошибок

Task создаёт процесс и позволяет безопасно управлять его жизненным циклом. Например:

task = Task.async(fn -> raise "бум!" end)
Task.await(task)

Это выбросит исключение в вызывающем процессе, если задача упала.

6. Супервизоры и восстановление

Супервизоры — ключевая часть системы. Они следят за дочерними процессами и могут их перезапускать. Пример:

children = \[
{MyWorker, \[\]}
\]
Supervisor.start_link(children, strategy: :one_for_one)

Если MyWorker падает, супервизор автоматически перезапускает его.

7. Причины завершения процесса

Процесс может завершиться с разными статусами:

  • :normal — штатное завершение.

  • :shutdown — при остановке узла или супервизора.

  • {:shutdown, reason} — расширенный сценарий.

  • Любая другая причина (:error, :badarg) считается ошибкой и вызывает аварийное завершение.

Можно задать причину явно:

exit(:some_reason)

8. Логирование падений

Ошибки процессов логируются в консоли/журнале. Если используется OTP и супервизоры, логи содержат стек вызовов и причину:

\[error\] Process #PID<0.123.0> terminating
\*\* (RuntimeError) что-то пошло не так

9. Необработанные сообщения остаются в mailbox

Если процесс падает, не обработав очередь сообщений — они теряются. Поэтому важна аккуратность в построении receive и логике обработки.

10. Пример обработки через супервизор

defmodule MyWorker do
use GenServer
def start_link(\_) do
GenServer.start_link(\__MODULE_\_, %{}, name: \__MODULE_\_)
end
def handle_call(:fail, \_from, state) do
raise "ошибка"
end
end
defmodule MyApp do
use Application
def start(\_, \_) do
children = \[{MyWorker, \[\]}\]
opts = \[strategy: :one_for_one, name: MySupervisor\]
Supervisor.start_link(children, opts)
end
end

Супервизор перезапустит MyWorker, если он упадёт при вызове :fail.

11. Поведение супервизоров (restart strategies)

  • :one_for_one — перезапускается только упавший процесс.

  • :one_for_all — все дочерние перезапускаются, если один упал.

  • :rest_for_one — падающий и все после него — перезапускаются.

12. Process.alive?/1 и проверка состояния

Можно проверить, жив ли процесс:

pid = spawn(fn -> :ok end)
Process.alive?(pid) # false — если уже завершился

13. Мониторинг процесса: Process.monitor/1

Можно отслеживать завершение процесса:

ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, \_pid, reason} -> IO.inspect(reason)
end

В отличие от link, монитор не завершает вызывающий процесс при падении другого. Это используется в Task.await/1.

14. Стратегия «пусть падает»

Философия Elixir/Erlang: вместо обработки всех ошибок — позволить процессу завершиться, а надёжная система (через супервизор) восстановит его автоматически. Это упрощает код, уменьшает количество условий и ошибок.