Эрланг на практике. Способы обработки ошибок. Let It Crash. — Эрланг на практике
Когда вся программа выполняется в одном потоке, аварийное завершение этого потока означает аварийное завершение программы. И если это случилось в месте, где явно не предусмотрена обработка ошибок, то остается минимум информации для диагностики проблемы.
Поэтому программисты стараются предусмотреть обработку всех возможных ошибок во всех возможных местах. Такой стиль программирования называется Defensive Programming. И он нередко он приводит к тому, что в программа содержит больше кода для обработки ошибок, чем кода, выполняющего основную задачу. Конечно, это усложняет и написание кода, и поддержку.
Эрланг предлагает другой подход: реализовать только основную задачу (happy path) и не писать код для обработки ошибок. Благодаря многопоточности и разделению потоков на рабочие и супервизоры, любая ошибка всегда будет замечена и записана в лог. А система в целом продолжит работу. Этот подход называется Let It Crash.
Между тем, все инструменты для Defensive Programming в эрланг есть. И полностью от этого подхода никто не отказывается. На практике каждый разработчик ищет свой баланс между Defensive Programming и Let It Crash.
В этом уроке разберемся, как оба подхода применяются в эрланг. Но сперва рассмотрим средства языка для работы с ошибками.
Типы данных Maybe/Option и Either/Result
Когда на 5-м уроке мы рассматривали Key-Value типы данных, мы заметили, что некоторые из них имеют по два варианта функций, возвращающих или обновляющих значение по ключу.
Например, dict:fetch/2 бросает исключение, если нет ключа в словаре, а dict:find/2 возвращает атом error. Аналогично ведут себя maps:get/2 и maps:find/2.
В эрланг есть некоторый бардак в поведении разных функций: proplists:get_value/2 возвращает Value | undefined, dict:find/2 и maps:find/2 возвращают | error, gb_trees:lookup/2 возвращает | none. Но общая закономерность видна: возвращается либо значение, обернутое в тегированый кортеж, либо некий атом, означающий отсутствие значения.
В других функциональных языка программирования это поведение стандартизировано.
Например, Haskell имеет тип Maybe:
а OCaml имеет тип Option:
Это псевдокод, а не правильный код на этих языках, но суть ясна.
Maybe/Option -- полезный тип во многих случаях. Но часто бывает нужно не просто сообщить об ошибке, но и указать тип ошибки. В эрланг для этого часто используются кортежи | .
А в Haskell есть тип Either:
и в OCaml есть тип Result:
throw, try..catch
Как в и большинстве языков программирования, в эрланг есть исключения и способ их перехватить и обработать. Но картина несколько усложняется тем, что есть три типа исключений и три разных способа их генерировать.
throw(Reason) -- генерирует обычное исключение. Чаще всего именно эту функцию используют разработчики.
erlang:error(Reason) -- генерирует фатальную ошибку, восстановление после которой не подразумевается, и текущий поток должен упасть. Впрочем, это скорее соглашение, нежели техническое отличие. Перехватить и обработать это исключение все равно можно.
exit(Reason) -- генерирует системное сообщение. Мы это обсуждали в 11-м уроке "Обработка ошибок на низком уровне" и помним, что с помощью системных сообщений реализуются связи между потоками. Необходимость вызывать exit/1 и вмешиваться в этот механизм возникает очень редко, разве что в целях тестирования.
Аргумент Reason во всех этих функциях может быть любой структурой данных. Обычно это кортеж, либо одиночный атом, несущий какую-то информацию об ошибке.
Перехватить исключения можно конструкцией try..catch:
После catch мы видим сопоставление с образцом, но не совсем обычное. TypeOfError -- это тип исключения (throw, error или exit), а Reason1 -- это шаблон, который должен совпасть с аргументом Reason соответствующих функций.
Вот более конкретный пример, как могут вылядеть эти шаблоны:
При обработке исключения нас обычно интересует стек вызовов функций. Получить его можно вызовом erlang:get_stacktrace().
Чтобы понять, как этот стек выглядит, лучше всего увидеть его на практике.
Возьмем такой простой модуль из 3-х функций:
Стек представляет собой список кортежей, где каждый кортеж указывает функцию и строку в исходном коде. Мы видим, что исключение возникло в модуле test, в функции other_fun с арностью 0, которая определена в файле test.erl, в 20-й строке кода. И дальше мы видим цепочку вызовов функций, которые привели к этому месту.
Выбор способа обработки ошибок
Итак, у разработчика есть несколько вариантов.
Внутри функции можно сообщить об ошибке с помощью исключения, либо возвратом специального значения (Option или Result). А снаружи, при вызове функции, можно либо обработать ошибку (Defensive Programming) либо проигнорировать (Let It Crash).
В эрланг, и вообще в функциональном программировании, предпочитают использовать специальные значения, а исключениями применяют редко. Можно обойтись и вообще без исключений, что подтверждает язык Go. Но это не всегда удобно.
Например, сервер обрабатывает HTTP запрос со сложными входными данными. Эти данные нужно валидировать по многим условиям, и при несоответствии данных любому из этих условий, сервер отказывается их принимать.
Вариант без исключений может выглядеть примерно так:
Причем, таких проверок может быть десяток и больше. Проблему можно решать по-разному. Например, применив монады (библиотека erlando реализует для эрланг некоторые монады из Haskell). Это даст лаконичный, но более сложный для понимания код.
А с помощью исключений можно сделать простое и понятное решение:
В функциональном программировании считается правильным иметь только одну точку выхода из функции. Но на практике иногда удобно иметь много точек выхода. И в этой ситуации помогают исключения.
Я могу дать такую рекомендацию: в большинстве случаев использовать специальные типы Option и Result. Исключения использовать редко, только если они дают более простой и понятный код, чем код со специальными типами.
Итак, мы сообщили об ошибке. Следующий вопрос: нужно ли обрабатывать эту ошибку, или лучше игнорировать ее? Тут нужно четко понимать, что происходит, если ошибка проигнорирована.
А происходит следующее: текущий поток падает и перезапускается супервизором из некоего известного стабильного состояния. Информация, хранящаяся в памяти потока (стек и куча), теряется. В лог пишется сообщение об ошибке.
Часто нас такое поведение устраивает. И тогда лучше не усложнять код и игнорировать ошибку.
Но есть случаи, когда это поведение не подходит:
- ошибка может быть не ошибкой, а штатной ситуацией, для которой мы знаем, что нужно делать;
- нужно сохранить данные из памяти потока;
- информации в логе недостаточно, чтобы понять, что происходит.
И тогда нужно явно обработать ошибки.
catch all шаблоны
Отдельная, но близкая к обработке ошибок тема -- использование catch all шаблонов при сопоставлении с образцом.
Рассмотрим такой код:
Это Let It Crash подход. Если some(Arg) вернет какой-то результат, для которого нет шаблона, то поток упадет.
Это Defensive Programming. Результат, для которого нет шаблона мы явно обрабатываем каким-то образом.
Я рекомендую использовать catch all шаблоны для обработчиков handle_call, handle_cast, handle_info в gen_server. Это позволяет четко логировать запросы к gen_server, для которых не реализована обработка.
Информацию об ошибке мы получим в любом случае.
Если есть catch all шаблон:
Но в первом случае это будет просто аккуратная запись в error.log. А во втором случае gen_server упадет и потеряет свое состояние.
Supervisor и распределенность
Супервизоры мы уже рассматривали в 11-м и 12-м уроках. Повторяться не буду. Добавлю только, что если считать try..catch первым уровнем обработки ошибок, то супервизоры будут вторым уровнем.
Для кода в стиле Let It Crash супервизоры -- главное средство. Но важно, чтобы каждый рабочий поток был запущен под супервизором.
Распределенность обеспечивает третий уровень обработки ошибок. Правильно построенный кластер продолжает обслуживать клиентов при выходе из строя одного или даже нескольких узлов. Здесь работают похожие механизмы. Узлы в кластере мониторят друг друга подобно тому, как мониторят друг друга потоки в рамках одного узла.
В итоге эрланг позволяет построить очень надежные системы. Но надежность не возникнет сама собой, это должен сделать разработчик.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты.
Нашли опечатку или неточность?Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Что-то не получается или материал кажется сложным?Загляните в раздел «Обсуждение»:
- задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
- расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
- изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Урок «Как эффективно учиться на Хекслете»
- Вебинар «Как самостоятельно учиться»
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.