Как стать автором
Обновить

Слежение за процессами и обработка ошибок, часть 1

Время на прочтение6 мин
Количество просмотров4K

0 Преамбула


Согласитесь приятно, когда в хозяйстве все под контролем и все в порядке, каждая вещь стоит на своем месте и четко выполняет свое вселенское предназначение. Сегодня мы рассмотрим вопросы организации порядка в огромном множестве процессов эрланга. Базовые понятия о процессах эрланга можно прочитать в этом посте.

1 Ты следишь за мной – я слежу за тобой


Все, кто так или иначе знакомился с эрлангом, слышал фразу: «Пусть процесс упадет, а другой что-нибудь сделает с этим или разберется с проблемой». Согласитесь, когда что-то ломается – это плохо, а если мы ещё об этом и длительное время не знаем, то это плохо вдвойне. Сломала ваша кошка свою миску с молоком и сокрыла этот ужасный факт от вас – плохо! Пусть миска следит за кошкой, а кошка за миской. Да простят читатели автора за такое грубое сравнение. Итак, перейдем к делу.

Связь процессов между собой для слежения за состоянием друг друга – это одна из базовых концепций эрланга. В сложной и хорошо спроектированной системе ни один процесс не должен «висеть в воздухе». Все процессы должны быть встроены в дерево контроля, листьями которого являются рабочие процессы, а внутренние узлы следят за рабочими (контроллеры) [2 |см. принципы OTP (Open Telecom Platform)]. Хотя можно сделать, чтобы и два рабочих были связаны.


Рисунок 1

Если не подниматься на уровень абстракции, который предоставляет OTP, в эрланге есть два механизма связи процессов:
  1. Связь (link) – двунаправленная связь между двумя процессами.
  2. Мониторы – однонаправленная связь процесса-наблюдателя и наблюдаемого.

1.1 Связи


Для создания связей между процессами используются следующие функции:
  • erlang:link/1 – создание связи между вызывающим функцию и другим процессом;
  • erlang:spawn_link/1/2/3/4 (есть так же псевдоним proc_lib:spawn_link/1/2/3/4) – создание нового процесса и прилинковка его к процессу, вызывающему функцию;
  • erlang:unlink/1 – удаление связи между процессом, вызывающим функцию, и указанным в аргументах;
  • pool:pspawn_link/3 – создание нового процесса на одном из узлов в пуле и прилинковка его к процессу, вызывающему функцию.

Что дает нам двунаправленная связь между процессами? Связи определяют путь распространения ошибок. Один процесс умер, второй об этом узнал и в ряде случаев, которые мы рассмотрим ниже, он тоже завершит свою работу, разослав сигнал всем остальным процессам, которые к нему так же привязаны. Данный механизм позволяет дистанционно обрабатывать ошибки, т.е. обработчиком может быть отдельный процесс (контроллер), к которому все эти ошибки будут «стекаться» по связям, причем процесс-обработчик может находиться вообще на другом узле. И все эти вкусности почти бесплатно – все уже реализовано в платформе, нам лишь остается правильно построить нашу мега-супер-отказо-распределенную систему.


Рисунок 2

Когда процесс падает (см. рисунок 2) отправляется сигнал выхода всем прилинкованным процессам, данный сигнал содержит информацию, какой процесс и по какой причине погиб в бою. Сигнал представляет собой кортеж {‘EXIT’, Pid, Reason}.

Существует два предопределенных значения переменной Reason:
  • normal – данное значение причины устанавливается, если процесс выполнил всю работу, которой мы его нагрузили, т.е. попросту достиг конца функции, с которой он был вызван. В этом случае процессы, которые слинкованы с ним, не завершат свою работу.
  • kill – не перехватываемый сигнал, который всегда убивает процесс, даже системный, используется для принудительного завершения сбойных процессов.

Для того чтобы процесс мог перехватывать сигналы выхода, его необходимо сделать системным поставив флаг trap_exit с помощью вызова функции process_flag(trap_exit, true).

Итак, хватит теории, давайте все попробуем на практике. Открываем наш любимый редактор и создаем небольшой модуль. Давайте сначала протестируем нормальное завершение процесса. Для простоты эксперимента в качестве одного из процессов у нас будет shell.

-module(links_test).
-export([start_n/1, loop_n/1]).

start_n(Sysproc) ->
	%% test normal reason
	process_flag(trap_exit, Sysproc),
	io:format("Shell Pid: ~p~n", [self()]),
	Pid = spawn_link(links_test, loop_n, [self()]),
	io:format("Process started with Pid: ~p~n", [Pid]).

loop_n(Shell) ->
	%% loop for test normal reason
	receive
		after 5000 ->
				Shell ! timeout
	end.


В модуле определено две функции: первая start_n создает новый процесс и линкует его с вызывающим процессом (в нашем случае это будет shell), в качестве параметра принимает значение boolean, которое делает процесс системным. Вторая loop_n – это тело создаваемого процесса, в качестве аргумента мы передаем ему Pid вызывающего процесса (shell). Через 5 секунд после запуска процесса он отправляет шелу сообщение timeout. Компилируем и запускаем наш процесс.

(emacs@aleksio-mobile)2> links_test:start_n(false).
Shell Pid: <0.36.0>
Process started with Pid: <0.43.0>
ok
(emacs@aleksio-mobile)3> flush().
Shell got timeout
ok
(emacs@aleksio-mobile)4>


Вызываем функцию links_test:start_n с параметром false, т.е. shell не системный процесс и сигналы выхода ловить не может. Видим, что процесс был успешно создан, т.к. в функции loop_n нет хвостовой рекурсии, она успешно отработает и процесс завершится. Вызываем функцию flush(), чтобы сбросить все сообщения из ящика shell, и видим, что было получено сообщение от нашего процесса «Shell got timeout». Никаких сигналов выхода мы не видим, так как флаг обработки данного вида сигналов не был установлен. Теперь сделаем shell системным процессом.

(emacs@aleksio-mobile)5> links_test:start_n(true).
Shell Pid: <0.36.0>
Process started with Pid: <0.51.0>
ok
(emacs@aleksio-mobile)6> flush().
Shell got timeout
Shell got {'EXIT',<0.51.0>,normal}
ok
(emacs@aleksio-mobile)8>


После выполнения функции видим, что помимо сообщения timeout было получено сообщение о нормальном завершении от нашего процесса {'EXIT',<0.51.0>,normal}. Замечательный механизм, позволяет нам сэкономить на количестве кода, когда необходимо узнать, что процесс выполнил свою работу (не надо самим отправлять сигнал «Я все сделал»).

Теперь давайте попробуем сгенерировать ошибку, отличную от normal. Модифицируйте код модуля как в листинге ниже.

-module(links_test).
-export([start_n/1, loop_n/1]).

start_n(Sysproc) ->
	%% test abnormal reason
	process_flag(trap_exit, Sysproc),
	io:format("Shell Pid: ~p~n", [self()]),
	Pid = spawn_link(links_test, loop_n, [self()]),
	io:format("Process started with Pid: ~p~n", [Pid]).

loop_n(Shell) ->
	%% loop for test abnormal reason
	receive
		after 5000 ->			
				Shell ! timeout,
				1 / 0
	end.


Мы очень суровы и решили сделать деление на ноль, компилятор естественно нас предупредит, что мы не правы, но мы его предупреждение просто проигнорируем.

(emacs@aleksio-mobile)33> links_test:start_n(false).
Shell Pid: <0.117.0>
Process started with Pid: <0.120.0>
ok
(emacs@aleksio-mobile)34> ** exception error: bad argument in an arithmetic expression
     in function  links_test:loop_n/1
(emacs@aleksio-mobile)34> 
=ERROR REPORT==== 25-Feb-2011::16:22:48 ===
Error in process <0.120.0> on node 'emacs@aleksio-mobile' with exit value: {badarith,[{links_test,loop_n,1}]}
(emacs@aleksio-mobile)34> flush().
ok
(emacs@aleksio-mobile)35> self().
<0.122.0>
(emacs@aleksio-mobile)36>


Обратите внимание Shell Pid = <0.117.0>. Через 5 секунд вываливается ошибка, объясняющая, что все-таки мы были не правы. Попробуем посмотреть, что же в очереди у shell, а там пусто. Где же наше письмо timeout? Выполним команду self(), Shell Pid теперь равен <0.122.0> — это значит, что наш сбойный процесс послал сигнал выхода shell с причиной {badarith,[{links_test,loop_n,1}]}, а так как shell в данном примере не системный процесс, он благополучно упал и был перезапущен каким-то контроллером (каким мы возможно рассмотрим в следующих статьях). Теперь включим флаг обработки сигналов выхода.

(emacs@aleksio-mobile)40> links_test:start_n(true).
Shell Pid: <0.132.0>
Process started with Pid: <0.139.0>
ok
(emacs@aleksio-mobile)41> 
=ERROR REPORT==== 25-Feb-2011::16:34:19 ===
Error in process <0.139.0> on node 'emacs@aleksio-mobile' with exit value: {badarith,[{links_test,loop_n,1}]}
(emacs@aleksio-mobile)41> flush().
Shell got timeout
Shell got {'EXIT',<0.139.0>,{badarith,[{links_test,loop_n,1}]}}
ok
(emacs@aleksio-mobile)42> self().
<0.132.0>
(emacs@aleksio-mobile)43>


Думаю, комментарии к результатам излишни, тут все понятно.

Мы разобрали четыре случая:
Сигнал trap_exit Причина завершения процесса Действие процесса, который остался «жив»
true normal В почтовый ящик приходит сообщение {'EXIT', Pid, normal}
false normal Процесс продолжает свою работу
true Любая отличная от normal и kill В почтовый ящик приходит сообщение {'EXIT', Pid, Reason}
false Любая отличная от normal и kill Процесс умирает, рассылая сигнал выхода по всем своим связям (т.е. происходит распространение ошибки)

Заключение


В следующих статьях (часть 2, часть 3) мы рассмотрим механизм мониторов и постреляем по процессам сигналами kill. Хотелось бы услышать мнение хаброжителей, статьи по каким темам эрланга вам будут наиболее интересны?

Список литературы


1. Отличная интерактивная документация.
2. Принципы OTP.
3. ERLANG Programming by Francesco Cesarini and Simon Thompson.
4. Programming Erlang: Software for a Concurrent World by Joe Armstrong.
Теги:
Хабы:
+25
Комментарии4

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн