Что произойдёт, если процесс "падает"?
В 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: вместо обработки всех ошибок — позволить процессу завершиться, а надёжная система (через супервизор) восстановит его автоматически. Это упрощает код, уменьшает количество условий и ошибок.