Состояние в процессах, связи и мониторинг
У процессов нет переменных, которые «живут между вызовами». Состояние держат в аргументах цикла. А связи и мониторы превращают падения процессов в управляемые события.
Состояние процесса — это его «память». Передавайте его параметром цикла, обновляя при каждой итерации. А links и monitors — нервная система отказоустойчивости.
Чтобы процесс хранил состояние, он передаёт его в рекурсивный loop и обновляет на каждом сообщении:
defmodule Counter do
def start(initial), do: spawn(fn -> loop(initial) end)
defp loop(count) do
receive do
:inc -> loop(count + 1)
{:get, from} ->
send(from, {:count, count})
loop(count)
end
end
end
Связь (spawn_link) делает два процесса «связанными»: падение одного посылает сигнал выхода другому. Монитор — односторонний: вы получаете сообщение, когда наблюдаемый процесс упал, но сами не падаете.
# Монитор: узнаём о падении, не падая сами
{pid, ref} = spawn_monitor(fn -> raise "boom" end)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
IO.puts("Процесс упал: #{inspect(reason)}")
end
Как работает под капотом (BEAM)
Состояние «между сообщениями» хранится не в переменной, а в аргументе хвостового цикла: каждая итерация получает свежее неизменяемое значение. Связи (links) двунаправлены: если связанный процесс падает, по связи бежит сигнал выхода, и по умолчанию получатель тоже падает — так сбой «всплывает» к супервизору. Мониторы односторонни и не валят наблюдателя: вместо этого он получает сообщение {:DOWN, ref, ...}. Эти примитивы — фундамент, на котором супервизоры строят автоматический перезапуск: именно через links падение ребёнка доходит до супервизора.
Связь (link), двусторонняя:
[A] === [B] падение A => сигнал выхода в B (B тоже падает)
Монитор (monitor), односторонний:
[Наблюдатель] ...мониторит...> [B]
падение B => {:DOWN, ...} в почту наблюдателя (он жив)
Та же идея на Python ▶
Состояние в цикле и реакцию на «падение» подчинённого смоделируем явно.
# Состояние живёт в параметре "цикла", а не в глобальной переменной
def counter_loop(messages, count=0):
for msg in messages:
match msg:
case "inc": count += 1
case ("get", box): box.append(count) # "ответ" наблюдателю
return count
box = []
final = counter_loop(["inc", "inc", ("get", box), "inc"])
print(box) # [2] — состояние на момент get
print(final) # 3
# "Монитор": ловим падение подчинённого, сами не падаем
def supervise(task):
try:
task()
except Exception as e:
return ("DOWN", str(e)) # как сообщение {:DOWN, ...}
return ("UP", None)
print(supervise(lambda: (_ for _ in ()).throw(RuntimeError("boom"))))
# ('DOWN', 'boom')
Частые ошибки
- Хранить состояние «снаружи». Состояние — в аргументе цикла; глобальных мутируемых переменных нет.
- Путать link и monitor. Link валит вас при падении партнёра; monitor лишь уведомляет.
- Ставить голый link без супервизии. Связь без стратегии перезапуска просто «утянет» вас за собой.
Best practices
- Передавайте состояние параметром хвостового цикла и обновляйте на каждом сообщении.
- Используйте monitor, когда нужно лишь узнать о падении; link — когда судьбы процессов должны быть связаны.
- Не городите links вручную для надёжности — это работа супервизоров OTP.
Итог. Состояние в цикле, links и monitors — это сырые примитивы, на которых стоит вся отказоустойчивость BEAM. Писать их руками утомительно, поэтому OTP даёт готовые абстракции. К ним — в финальном разделе.
От ручных примитивов к OTP
Этот урок показывает «сырьё», из которого сделана отказоустойчивость: цикл с состоянием в аргументе, links для распространения сбоев, monitors для уведомлений. Понимать их важно, но писать руками каждый раз — нет. Почти всё, что мы здесь собрали вручную, в реальном коде делают абстракции OTP: GenServer ведёт цикл и хранит состояние за вас, супервизор расставляет links и реализует стратегию перезапуска, мониторы прячутся внутри библиотечных функций.
Тем не менее эти знания не пропадут зря. Когда вы будете отлаживать, почему «упал один процесс, а за ним посыпались соседние», вы вспомните про links и распространение сигналов выхода. Когда понадобится узнать о завершении задачи, не связывая с ней свою судьбу, вы возьмёте monitor. OTP не отменяет эти примитивы — он строится на них и иногда требует спуститься на их уровень. Так что считайте этот урок взглядом «под капот» того, что в следующем разделе станет одной строкой use GenServer.