Надзор — фундаментальная возможность Erlang/OTP, позволяющая одним акторам следить за другими. То же: супервизия, supervision.
Необходимость надзора обоснована Джо Армстронгом в его диссертации (pdf). В ней отражена история создания Эрланга, философия и пр. Краеугольный камень диссертации следует из её названия: «Создание надёжных распределённых систем при наличии программных ошибок». Программные ошибки неизбежны, конечно. С ними надо бороться, но то, что мы намерены с ними бороться, не означает, что их нет в настоящее время. Следовательно, так надо строить нашу Систему, чтобы ошибки не парализовали её полностью, их значение должно быть локальным, и должны быть механизмы быстрого исправления ошибки.
Есть две роли у процессов: работник и надзиратель. Работник выполняет основную полезную нагрузку. Его код настроен лишь на выполнение этой полезной нагрузки, и даже локальная обработка ошибок (try ... catch) встречается очень редко. Поэтому код работника строен и изящен. Если работник допускает ошибку, то и ладно. Парадоксальный принцип Армстронга “Пусть это рухнет!” следует из сверхзадачи — построения постоянно работающих и максимально надёжных систем. Всё равно ошибки будут, надо лишь ограничить их вредоносное влияние на Систему. Эту задачу берут на себя надзиратели.
В приложении, построенном согласно принципам OTP, есть древо надзора. Есть главный актор-надзиратель. Он следит за рядом (обычно несколько) других акторов. Среди этих акторов могут быть как акторы-работники, так и акторы — другие надзиратели. Эти другие надзиратели в свою очередь следят за другими акторами, среди которых также встречаются работники и надзиратели. И так далее. Если какой-то актор (работник или надзиратель) рухнет, его непосредственный “начальник” может его перезапустить.
Акторы-надзиратели должны осуществлять поведение supervisor.
Поднадзорные акторы могут быть определены статически, то есть в исходном коде надзирателя, и тогда он будет надзирать за этим конкретным списком акторов. Однако есть возможность реализовать (в дополнение или вместо статического надзора) динамический надзор, когда заранее полный список поднадзорных акторов неизвестен. Эрланг предназначен для разработки постоянно работающих систем, поэтому нет смысла останавливать Систему только для того, чтобы добавить нового актора.
Чтобы воочию увидеть, как надзиратель восстанавливает работника, создадим три актора:
Код надзирателя:
-module(super_1).
-behaviour(supervisor).
-export([init/1, start_link/0]).
init([]) ->
io:format("super1 started~n"),
SupFlags = #{strategy => one_for_one, intensity => 3, period => 1},
ChildSpecs = [
#{id => worker_statem,
start => {worker_statem, start_link, []},
type => worker,
restart => permanent,
shutdown => brutal_kill},
#{id => worker_1,
start => {worker, start_link, [worker_1]},
type => worker,
restart => permanent,
shutdown => brutal_kill},
],
{ok, {SupFlags,ChildSpecs}}.
start_link() ->
supervisor:start_link({local,?MODULE}, ?MODULE, []).
За функцию super_1:start_link/0 мы будем дёргать, чтобы запустить надзирателя и чтобы он в свою очередь запустил своих потомков, то есть поднадзорные акторы. init/1 — колбэк, он запускается автоматически, когда создаётся актор. У функции supervisor:start_link/3 следующие аргументы: название, которое мы даём надзирателю, название модуля, из которого с помощью init/1 создаётся надзиратель, дополнительные аргументы.
Функция init/1 должна возвращать кортеж {ok, {SupFlags,ChildSpecs}}. Здесь SupFlags — флаги надзора, то есть характеристики работы создаваемого надзирателя в целом. ChildSpecs — это список карт, каждая из которых настраивает надзор по отношению к конкретному потомку (создаваемому поднадзорному актору).
Данные флаги надзора говорят о том, что мы выбрали стратегию воссоздания акторов one_for_one, то есть будет воссоздаваться только рухнувший актор. Также мы установили предел: за одну секунду (period) должно быть не более трёх (intensity) воссозданий рухнувших акторов. Если этот предел будет достигнут, наш надзиратель вместе со своими потомками будет остановлен.
У каждого создаваемого потомка своя карта параметров. id — внутренний идентификатор актора. start — МФА для запуска (и перезапуска) актора. type — тип создаваемого актора (worker или supervisor). restart — при каких обстоятельствах респаунить актор (permanent — всегда). shutdown — насколько грубо обрывать жизнь актора.
Акторы-потомки в общем должны создаваться так, словно они ничего не знают о надзирателе над ними.
-module(worker_statem).
-behaviour(gen_statem).
-define(NAME, ?MODULE).
-export([init/1,callback_mode/0]).
-export([start_link/0, terminate/3]).
-export([state_123/3]).
start_link() ->
gen_statem:start_link({local,?NAME}, ?MODULE, [], []).
init(_) ->
io:format("worker_statem started.~n"),
Data = #{},
{ok, state_123, Data}.
callback_mode() ->
state_functions.
state_123(cast, Mess, Data) ->
io:format("Got cast! ~p~n", [Mess]),
{next_state, state_123, Data, [{state_timeout,5_000,lock}]};
state_123(state_timeout, lock, Data) ->
io:format("Timeout: ~p~n", [1/(rand:uniform(6)-1)]),
{next_state, state_123, Data}.
terminate(_Reason, _State, _Data) ->
io:format("worker_statem terminated."),
ok.
Данный модуль реализует поведение gen_statem, необходимость функции init/1 продиктована именно этим поведением (машина состояний). Запуск актора происходит через функцию gen_statem:start_link/4. Она с помощью init/1 инициализирует актор, и дальше раз за разом вызывается лишь функция state_123. В ней и содержится весь полезный функционал нашего работника. Иногда он получает cast-сообщения, о чём даёт обратную связь и после чего программирует сам себя, чтобы через 5 секунд получить таймаут-сообщение. Когда такое сообщение получено, он даёт об этом обратную связь, при этом в каждом шестом случае актор рушится.
Ниже представлен другой работник — не реализующий никакого поведения.
-module(worker).
-export[start_link/1, run/1, loop/1].
start_link(Name) ->
Pid = spawn_link(?MODULE, run, [Name]),
register(Name, Pid),
{ok,Pid}.
run(Name) ->
io:format("~p started~n", [Name]),
loop(Name).
loop(Name) ->
io:format("~p works~n", [Name]),
case 1/(rand:uniform(6)-1) of
1.0 -> gen_statem:cast(worker_statem, {from, Name});
_ -> []
end,
receive
after 10_000 -> ?MODULE:loop(Name)
end.
Я решил, что этот код далее будет использован при создании нескольких работников, поэтому при создании используется Name — имя работника. В документации сказано, что создание работников (в данном случае функция worker:start_link/1) должно завершаться вызовом одной из следующих функций:
Совместимость означает следующее. Функция запуска должна создать дочерний процесс и связать линком (поэтому мы использовали не spawn, а spawn_link), Функция запуска должна вернуть {ok,ChildPid} или {ok,ChildPid,Info}. Также эта функция может вернуть атом ignore, если по какой-то причине актор не может быть запущен. Также, если что-то пойдёт не так, она может вернуть {error,Error}.
Регистрация нужна актору хотя бы для того, чтобы потом, когда древо надзора будет существовать внутри приложения, Обсервер показывал древо, в котором у каждого актора указан не пид, а имя. Так более наглядно.
Запуск нашего актора начинается с единоразового выполнения run/1, где можно совершить какую-нибудь первоначальную настройку актора. После этого управление передаётся в вечный цикл loop/1. Оператор case ... end нам нужен по двум причинам:
— чтобы что-то посылалось другому работнику (worker_statem), так мы имитируем совместную работу работников; - чтобы иногда актор рушился — когда на нашем виртуальном игральном кубике выпадает единица.
Я нарочно сделал, чтобы оба актора периодически рушились, так мы увидим в действии работу надзирателя по воссозданию поднадзорных акторов.
Компилируем и запускаем надзирателя.
1> super_1:start_link().
super1 started
worker_statem started.
worker_1 started
{ok,<0.85.0>}
worker_1 works
worker_1 works
...
worker_1 works
worker_1 started
=SUPERVISOR REPORT==== 15-Sep-2025::14:45:50.245654 ===
...
worker_1 works
=ERROR REPORT==== 15-Sep-2025::14:45:50.245571 ===
Error in process <0.90.0> with exit value:
{badarith,[{worker,loop,1,[{file,"worker.erl"},{line,14}]}]}
worker_1 works
...
Got cast! {from,worker_1}
worker_statem terminated.=ERROR REPORT==== 15-Sep-2025::14:46:25.258363 ===
** State machine worker_statem terminating
** Last event = {state_timeout,lock}
** When server state = {state_123,#{}}
** Reason for termination = error:badarith
...
=CRASH REPORT==== 15-Sep-2025::14:46:25.258926 ===
crasher:
initial call: worker_statem:init/1
pid: <0.86.0>
registered_name: worker_statem
exception error: an error occurred when evaluating an arithmetic expression
...
worker_statem started.
...
Теперь попробуем оформить этот пример в виде приложения. Нам это даст хотя бы то, что можно будет в Обсервере увидеть красивое древо надзора.
Для начала исправим модуль super_1. Мы бы могли создать другой модуль, обладающий поведением application, но не будем зря создавать файлы. Нам нужно добавить следующие строки:
-behaviour(application).
-export([start/2, stop/1]).
start(_A,_B) ->
io:format("App super_1 starting...~n"),
?MODULE:start_link().
stop(_A) ->
io:format("App super_1 stoping...~n"),
ok.
Теперь этот модуль обладает как поведением supervisor, так и поведением application.
Также для приложения потребуется создать его ресурсный файл.
{application, super_1, [
{description, "My application with supervisor."},
{vsn, "0.02"},
{modules, [super_1, worker, worker_statem]},
{registered, [super_1]},
{applications, [kernel, stdlib]},
{mod, {super_1,[]}}
]}.
Запускаем и любуемся древом надзора в Обсервере.
1> application:start(super_1).
App super_1 starting...
super1 started
worker_statem started.
worker_1 started
ok
worker_1 works
2> observer:start().
ok

Сделав правый клик по тому или иному актору, можно получить подробную информацию о нём: пид, какая сейчас функция выполняется и др.
Copyright © 2025 Алексей Карманов