Сообщений 9    Оценка 805        Оценить  
Система Orphus

Обработка ошибок в Erlang

Автор: Joe Armstrong
Перевод: Тайкало Олег
Источник: Making reliable distributed systems in the presence of software errors
Материал предоставил: RSDN Magazine #3-2006
Опубликовано: 06.12.2006
Исправлено: 11.01.2007
Версия текста: 1.0
Исключения
Catch
exit
throw
Исправленные и неисправленные ошибки
Связи между процессами и мониторы процессов
Связи между процессами
Мониторы процессов

Выполнение функции в Erlang может привести к одному из двух результатов – или функция вернет значение, или же она сгенерирует исключение.

Генерация исключений может быть как неявной (выполненная системой исполнения Erlang), так и явной, путем вызова exit(X). Неявные исключения будут описаны в следующем разделе.

Вот пример неявной генерации исключения. Допустим, мы написали следующее:

factorial(0) -> 1;
factorial(N) -> N*factorial(N-1).

Вычисление factorial(10) вернет значение 3628800, а при вычислении factorial(abc) произойдет исключение {’EXIT’,{badarith,...}}.

Исключения заставляют программу оставить свои текущие дела, и начать делать что-то другое, именно поэтому они называются исключениями. Если мы напишем:

J = factorial(I)

мы ожидаем, что значение J будет значением функции factorial(I) в том случае, если I является числом. Если factorial вызывается с аргументом, который не является числом, выражение не имеет смысла. Следующий участок программы:

I = "monday",
J = factorial(I),

не имеет смысла, поскольку мы не можем вычислить factorial("monday"). Таким образом, J не имеет значения, и его бессмысленно вычислять.

Исключения

Исключения – это некорректные состояния, которые обнаруживаются Erlang-системой. Программы на языке Erlang компилируются в инструкции виртуальной машины, выполняемые эмулятором виртуальной машины, который является частью Erlang системы.

Когда эмулятор попадает в состояние, в котором он не может решить, что делать дальше, он генерирует исключение. Существует шесть типов исключений:

  1. Ошибки значения — это исключения типа “деление на ноль”. Аргумент функции имеет правильный тип, но некорректное значение.
  2. Ошибки типов — эти исключения возникают тогда, когда встроенная функция Erlang-а вызывается с аргументом некорректного типа. Например, встроенная функция atom_to_list(A) преобразовывает атом A в список целочисленных значений в кодировке ASCII, из которых состоит атом. Если A не является атомом, система генерирует исключение.
  3. Ошибки сопоставления с образцом — эти исключения возникают при попытке проверить соответствие структуры данных некоторому количеству образцов, а ни один из образцов не подходит. Это может произойти в заголовке функции, или при сопоставлении с образцом в конструкциях case, receive или if .
  4. Явные вызовы exit.
  5. Распространение ошибки — если процесс получает сигнал завершения, он может решить завершить свою работу и послать сигнал завершения всем процессам, с которыми он связан (см. пункт 3.5.6).
  6. Системные исключения — среда исполнения может прервать процесс, если у нее закончится свободная память, или если она обнаружит несоответствие в какой-нибудь внутренней таблице. Такие исключения не могут обрабатываться программистом.

Catch

Исключения могут быть преобразованы в значения с помощью примитива catch. Это можно продемонстрировать в интерпретаторе Erlang, попытавшись вычислить некорректное выражение, вычисление которого приведет к исключению. Мы попытаемся связать значение выражения 1/0 со свободной переменной X. Вот что происходит:

1> X = 1/0.
=ERROR REPORT==== 23-Apr-2003::15:20:43 ===
Error in process <0.23.0> with exit value:
{badarith,[{erl_eval,eval_op,3},{erl_eval,expr,3},
{erl_eval,exprs,4},{shell,eval_loop,2}]}
** exited: {badarith,[{erl_eval,eval_op,3},
{erl_eval,expr,3},
{erl_eval,exprs,4},
{shell,eval_loop,2}]} **

Ввод выражения X = 1/0 в оболочке Erlang приводит к генерации исключения и выводу сообщения об ошибке. Если мы попытаемся вывести значение переменной X , мы увидим следующее:

2> X.
** exited: {{unbound,’X’},[{erl_eval,expr,3}]} **

Конечно, X не имеет значения, поэтому произойдет еще одно исключение, и будет выведено еще одно сообщение об ошибке.

Чтобы преобразовать исключение в значение, мы выполняем его в блоке catch:

3> Y = (catch 1/0).
{’EXIT’,{badarith,[{erl_eval,eval_op,3},
{erl_eval,expr,3},
{erl_eval,exprs,4},
{shell,eval_loop,2}]}}

Теперь Y имеет значение, а именно кортеж из двух элементов. Первым элементом кортежа является атом EXIT, а вторым – выражение {badarith,...}. Y является обычным выражением Erlang, и может рассматриваться и использоваться как любая другая структура данных в Erlang. Выражение:

Val = (catch Expr)

вычисляет Expr в некотором контексте. Если вычисление функции завершается нормально, то catch вернет значение этого выражения. Если при вычислении функции произойдет ошибка, то выполнение функции немедленно прекращается, и генерируется исключение. Исключение – это объект Erlang, который описывает проблему, в данном случае значением catch будет значение сгенерированного исключения.

Если вычисление выражения (catch Expr) возвращает терм вида {’EXIT’,W}, мы говорим, что вычисление выражения было прервано по причине W.

Если исключение сгенерировано где-нибудь внутри блока catch, значением catch будет значение исключения. Если исключение генерируется вне блока catch, процесс, в котором было сгенерировано исключение, будет уничтожен, а исключение будет послано всем процессам, которые связаны с уничтожаемым процессом. Связи между процессами создаются путем вызова встроенной функции link(Pid).

exit

Явные исключения можно сгенерировать вызовом exit. Вот пример его использования:

sqrt(X) when X < 0 ->
    exit({sqrt,X});
sqrt(X) ->
...

Этот участок кода сгенерирует исключение {sqrt, X}, если будет вызван с отрицательным аргументом X.

throw

Примитив throw используется для изменения синтаксической формы исключения.

Throw может использоваться для того, чтобы отличить пользовательские исключения от исключений среды выполнения.

Исправленные и неисправленные ошибки

Допустим, мы написали:

g(X) ->
    case (catch h(X)) of
        {’EXIT’, _} ->
            10;
        Val ->
            Val
    end.
h(cat) -> exit(dog);
h(N)   -> 10*N.

При вычислении g(cat) происходит следующая последовательность событий:

  1. Вызывается h(cat) .
  2. h генерирует исключение.
  3. Исключение перехватывается в g.
  4. g возвращает значение.

Вычисление h(dog) приводит к следующему:

  1. Вызывается h(dog).
  2. N принимает значение dog в теле функции h.
  3. Вычисляется N*10, где N = dog.
  4. В операторе ’*’ генерируется исключение.
  5. Исключение распространяется в h.
  6. Исключение перехватывается в g.
  7. g возвращает значение.

Если внимательно посмотреть на этот пример, мы заметим, что при выполнении функции d(dog) произошло исключение, но оно было перехвачено и исправлено в g. Таким образом, мы можем сказать, что ошибка произошла, но была исправлена. Если бы мы вычисляли h(dog) напрямую, то произошла бы ошибка, которая не была бы перехвачена и исправлена.

Связи между процессами и мониторы процессов

Когда один из процессов в системе завершает свою работу, хотелось бы, чтобы другие процессы в системе получили уведомление об этом событии. Это необходимо, чтобы иметь возможность создавать отказоустойчивые системы. Существует два способа сделать это. Мы можем использовать либо связи между процессами, либо мониторинг процессов. Связи между процессами позволяют группировать множество процессов таким образом, что при возникновении ошибки в любом из процессов, все остальные процессы в группе будут уничтожены. Мониторы процессов позволяют отдельным процессам следить за другими процессами в системе.

Связи между процессами

Примитив catch используется для того, чтобы отлавливать ошибки, произошедшие внутри процесса. Теперь зададим вопрос: «А что случится, если catch самого верхнего уровня не сможет исправить ошибку, которую он обнаружит?» Ответ прост – процесс завершится.

Причина ошибки передается в аргументе исключения. Когда процесс завершается с ошибкой, причина ошибки посылается всем процессам, принадлежащим т.н. «множеству связей» завершающегося процесса. Процесс A может добавить процесс B в свое множество связей, используя встроенную функцию link(B). Связи симметричны в том смысле, что если A связан с B, то и B, в свою очередь, связан с A.

Связи также могут создаваться во время создания нового процесса. Если процесс A создает процесс B, используя следующий код:

B = spawn_link(fun() -> ... end),

то процесс B будет связан с A. Это семантически эквивалентно вызову функции spawn с немедленным последующим вызовом link, за исключением того, что эти две функции будут вызваны автоматически. Примитив spawn_link был введен для исправления очень редкой ошибки, которая может произойти, если процесс завершает работу в процессе создания, и не доходит до вызова функции link. Это может случиться, например, если процесс пытается создать другой процесс, использующий код несуществующего модуля.

Если процесс P завершает работу с необработанным исключением {’EXIT’, Why}, всем процессам из множества связей процесса P будет послан сигнал завершения {’EXIT’, P, Why}.

До сих пор здесь не говорилось о сигналах. Сигналы – это то, что посылается от процесса процессу при завершении работы одного из них. Сигнал – это кортеж вида{’EXIT’, P, Why}, где P – это Pid процесса, завершившего работу, а Why – терм, описывающий причину завершения процесса.

Любой процесс, получивший сигнал завершения, завершит свою работу, если значением Why не будет атом normal. Существует одно исключение из этого правила – если получающий процесс является системным процессом, то он не завершит свою работу, а поступивший сигнал будет преобразован в обычное сообщение, передаваемое между процессами, и это сообщение будет добавлено во входную очередь сообщений процессов. Вызвав встроенную функцию process_flag(trap_exit, true), обычный процесс может стать системным процессом.

Типичный фрагмент кода системного процесса, способного обрабатывать аварийные завершения других процессов, выглядит примерно так:

start() -> spawn(fun go/0).
go() ->
  process_flag(trap_exit, true),
  loop().
loop() ->
  receive
    {’EXIT’,P,Why} ->
      ... обработка ошибки ...
  end

Картину завершает один дополнительный примитив. exit(Pid, Why) посылает процессу с идентификатором Pid сигнал завершения с причиной завершения работы Why. Процесс, вызвавший функцию exit/2, не прекращает своей работы, так что такое сообщение может быть использовано в качестве «инсценированной смерти» процесса. Это особенность системы, а не ошибка!

Опять же существует исключение из правила, по которому системный процесс должен преобразовывать все сигналы в сообщения – выполнение exit(P, kill) посылает неостанавливаемый exit процессу P, который будет уничтожен с особой жестокостью. Такое использование exit/2 необходимо для того, чтобы уничтожать процессы, которые отказываются уважать запросы на завершение работы.

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

Мониторы процессов

Связи между процессами полезны для целых групп процессов, но не для ассиметричного мониторинга пар процессов. В типичной клиент-серверной модели отношение между клиентом и сервером ассиметрично по отношению к обработке ошибок. Предположим, что сервер участвует в нескольких долговременных сессиях с некоторым количеством клиентов; если сервер аварийно завершит свою работу, все клиенты тоже должны завершить работу, но если отдельный клиент аварийно завершит работу, мы не хотим, чтобы это сделал и сервер.

Для установки монитора можно использовать примитив erlang:monitor/2. Допустим, процесс A выполнит:

Ref = erlang:monitor(process, B)

Тогда, если B завершит свою работу с сообщением о завершении Why, A получит сообщение следующего формата:

{’DOWN’, Ref, process, B, Why}

Ни A, ни B не обязаны быть системными процессами для установки мониторов или для получения сообщений мониторинга.


Эта статья опубликована в журнале RSDN Magazine #3-2006. Информацию о журнале можно найти здесь
    Сообщений 9    Оценка 805        Оценить