От редактора
В далёком 2014 году уже был перевод этой статьи Кристиана Ньюманса, но впоследствии Ньюманс удалил старую статью, существенно дополнил её и опубликовал на Медиуме. Во-вторых, вероятно, после очередных обновлений Хабра, у старого текста поехало форматирование, и читать его стало тяжело. В общем, считаю, что статье необходимо дать второй шанс.
В этой статье описан принцип "Fail Fast!". Что это? Зачем он нужен? Как этот принцип поможет нам писать лучший код?
Всякий раз, когда в запущенном приложении происходит ошибка, есть три возможных подхода к её обработке:
Ignore! - ошибка попросту игнорируется, приложение продолжает свою работу как ни в чём не бывало.
Fail Fast! - приложение завершается с ошибкой.
Fail Safe! - приложение учитывает ошибку в своей работе и продолжает свою работу по наилучшему сценарию из возможных.
Какой подход является лучшим? Какой из них следует применять в приложении?
Прежде чем ответить на этот жизненно важный вопрос, давайте посмотрим на простой пример.
Допустим, нам нужно написать примитивное веб-приложение, которое будет отображаться рядом с фонтаном и показывать прохожим предупреждающее сообщение, о том, что вода в фонтане грязная. Вот код, который это делает:
<html>
<body>
<h2 style="color:red;">Important!</h2>
<p>Please <b>DO NOT</b> drink this water!</p>
</body>
</html>
Браузер покажет нам следующее сообщение:
Теперь добавим небольшой баг. Укажем <b>
вместо </b>
после фразы DO NOT:
<p>Please <b>DO NOT<b> drink this water!</p>
Возникают два интересных вопроса:
Что должно произойти?
Что произойдёт?
Ответ на второй вопрос получить довольно легко. Достаточно скормить наш код браузеру. Chrome, Edge, Firefox, Internet Explorer и Safari покажут нам следующее (на момент написания статьи):
Прежде, чем читать дальше, спросите себя: "Какой подход применяют браузеры?"
Очевидно, это не "Fail Fast!", поскольку приложение не сообщило об ошибке и продолжило, как ни в чём не бывало. Да, теперь больше текста стало отображаться жирным шрифтом, но текст по-прежнему корректен, и люди из фонтана не пьют. Не о чем беспокоиться!
Хорошо, давайте добавим другой баг. Вместо <b>
мы напишем <b
перед фразой DO NOT:
<p>Please <b DO NOT</b> drink this water!</p>
Упомянутые ранее браузеры покажут нам следующее:
Ужас! Теперь программа делает прямо противоположное, и последствия этому ужасны - приложение, призванное спасать жизни, превратилось в приложение-убийцу (но, к сожалению, не в то, которое каждый из нас мечтает когда-нибудь написать).
Важно осознавать тот факт, что приведённый выше пример не является преувеличением. Существует множество реальных случаев, когда мелкие баги приводили к катастрофе - например, космический корабль "Маринер-1", взорвавшийся через несколько мгновений после старта из-за пропущенного дефиса. Дополнительные примеры смотрите в Списке багов программного обеспечения.
Как мы видим, последствия отказа от "Fail Fast!" сильно отличаются и могут варьироваться от безвредных до катастрофических.
Итак, каков правильный ответ на вопрос: "Что должно произойти?"
Это зависит от ситуации. Но есть общие правила.
Правило первое:
Никогда не игнорируйте ошибку (не применяйте правило "Ignore!") - если для этого нет действительно веских причин.
В дополнительных пояснениях это правило не нуждается. Вспомним Заповедь Шестую из "Десяти Заповедей C-разработчика":
Если провозглашается функция, возвращающая код ошибки в трудный час, ты должен обработать ошибку, даже если это утроит твой код и вызовет боль твоих пальцев, ибо, если ты уповаешь, что "это не случится со мной", боги обязательно накажут тебя за высокомерие.
Правило второе:
На этапе разработки применяйте подход "Fail Fast!".
Обосновать это правило легко:
Подход "Fail Fast!" помогает при отладке. При любой ошибке выполнение кода прерывается, а сообщение об ошибке помогает обнаружить её, диагностировать и исправить. Поэтому подход "Fail Fast!" помогает написать надёжный код, снижает затраты на разработку и поддержку и предотвращает катастрофы в продакшене. Даже если ошибка не приводит к серьёзному сбою, всегда лучше обнаружить её как можно скорее, поскольку стоимость багфикса растёт в геометрической прогрессии вместе со временем, прошедшим в цикле разработки (компиляция, тестирование, вывод в продакшен).
Ошибки, возникающие на этапе разработки, обычно не приводят к серьёзным последствиям. Заказчик не жалуется, деньги не зачисляются на неправильный счёт, ракеты не взрываются.
"Fail Fast!" считается хорошей практикой в разработке ПО. Вот несколько подтверждающих цитат:
Поощряйте хорошие практики кодинга... Следствий этому много, в том числе, и "fail fast!"
- Команда Google Guava, "Объяснение философии"
Может показаться, что "немедленный и явный сбой" сделает Ваше приложение более уязвимым, но на самом деле, он сделает его более надёжным. Такие баги легче находить и исправлять, и их меньше попадёт в продакшен.
- Джим Шор, Мартин Фаулер, "Fail Fast"
Некоторые из сложнейших для выявления багов были вызваны кодом, который молча падает и продолжает работу, вместо того, чтобы выбросить исключение... Лучше выбросить исключение, как только сбой будет обнаружен.
Хенрик Варн, "18 уроков после 13 лет работы с Tricky Bugs"
Зачем ждать, пока выяснится, что что-то не работает? Сразу падайте...
- Джошуа Кериевский, "Введение в современный Agile"
Однако, ситуация может радикально измениться с выводом приложения в продакшен.
К сожалению, здесь единого правила не существует. Практика показывает, что по умолчанию лучше использовать подход "Fail Fast!". Ущерб, нанесённый приложением, которое игнорирует ошибку, обычно значительно превышает ущерб, нанесённый приложением, которое внезапно падает. К примеру, если банковское приложение падает, пользователь злится. Но если банковское приложение отображает неправильный баланс, пользователь очень злится. "Злой" лучше, чем "очень злой". Поэтому, лучше использовать "Fail Fast!".
В нашем примере с HTML-кодом подход "Fail Fast!" так же более оправдан. Предположим, что вместо сообщения браузеры выводили бы сообщение об ошибке. Разработчики сразу бы узнали о проблеме и быстро её исправили. Но даже если глючный код по каким-то причинам попадёт в продакшен, последствия будут не так уж и страшны. Призыв "Please drink this water" может иметь катастрофические последствия, в то время, как отсутствие сообщения вообще или отображение непонятной ошибки приведёт к тому, что очень малый процент прохожих выпьют воду из фонтана.
На практике же, каждый конкретный случай требует индивидуального подхода. И это особенно касается приложений, способных нанести большой ущерб - например, в медицинских, банковских приложениях или приложениях для захвата космоса. Применение "Fail Fast!" обосновано ровно до того момента, пока наша ракета не начала взлетать. Как только произошёл старт, остановить приложение (или, хуже того, проигнорировать ошибку) уже не вариант. Тут в свои права вступает подход "Fail Safe!".
Иногда хороший вариант: упасть, но минимизировать ущерб. К примеру, когда падает ваш текстовый редактор, он сначала сохраняет текущий файл во временный, затем показывает пользователю сообщение: "Бла-бла-бла, Ваши изменения были сохранены во временный файл abc.temp", отправляет отчёт об ошибке разработчикам, и только потом падает.
Отсюда третье правило:
В критических приложениях подход "Fail Safe!" должен быть реализован для того, чтобы свести к минимуму ущерб.
Подведём итог.
На этапе разработки используем только "Fail Fast!".
В продакшене: по умолчанию используем "Fail Fast!"; критически важные приложения, несущие риск большого ущерба в случае сбоя, нуждаются в индивидуальном, зависящем от контекста и исключающем (или минимизирующем) ущерб поведении. В отказоустойчивых системах должны применяться подходы "Fail Safe!" и "React Appropriately!" ("Реагируйте адекватно!")
Та же идея выражена в великолепном "Правиле исправления" книги "Искусство программирования Unix" Эрика Стивена Реймонда:
Исправляйте всё, что можно - но если нужно упасть, падайте громко и как можно скорее.
Примечание: больше информации и примеров доступно в Википедии: Fail fast, Fail safe, и Fault tolerant computer system.
В любом случае, очень полезно использовать среду разработки, поддерживающую принцип "Fail Fast!". К примеру, его поддерживают компилируемые языки, поскольку компиляторы сообщают об ошибках компиляции. Вот пример глупой ошибки, которая ускользает от человеческого глаза, но может привести к бесконечному циклу:
var row_index = 1
...
row_indx = row_index + 1
Подобные опечатки легко обнаруживаются приличным компилятором или (что ещё лучше) интеллектуальной IDE.
К счастью, существует огромное количество "Fail Fast!"-фич, встроенных в язык. Все они основаны на правиле:
Ошибки необходимо обнаруживать во время компиляции или как можно раньше во время работы приложения.
Вот примеры мощных "Fail Fast!" языковых фич: статическая и семантическая типизация, null-safety при компиляции, дженерики, встроенное модульное тестирование и так далее.
Но чем обнаруживать ошибки на ранней стадии, лучше не допускать их по замыслу. Это достигается, когда язык программирования не поддерживает уязвимые методы программирования - к примеру, глобальные изменяемые данные, неявная типизация, молчаливое игнорирование арифметических ошибок переполнения, достоверность (например, "", 0 и null равны false) и так далее.
Поэтому всегда нужно отдавать предпочтение окружению (языки программирования / библиотеки / фреймворки / инструменты), которое поддерживает принцип "Fail Fast!". Нам придётся меньше заниматься отладкой, а наш код будет надёжнее и безопаснее за меньшее время.
Комментарии (9)
Samhuawei
03.11.2022 11:03В эру кубернетиса и облаков надо как можно быстрее упасть чтобы соседи перехватили знамя и понесли дальше. Я бы даже советовал выделять ресурсов по-минимуму чтобы при малейшем признаке фатала приложение завершалось и кубернетис перестартовал всё что можно.
Для клиентских приложений конечно можно и похромать какое-то время, чисто чтобы юзер сохранил данные. Но сколько осталось тех клиентских? Даже фотошоп в облаке.a-tk
03.11.2022 12:22+1Не кубером единым жив мир.
Согласитесь, потеря документа, над которым вы работаете на локальном компьютере - это плохо.
Да даже проще: у вас из-за ошибки разметки на любой странице или в скрипте шустро рестартует браузер в состоянии "с чистого листа" или хотя бы как он был при последнем ручном закрытии (ну, сохраняем конфиг не постоянно, а по фиксированному событию).
a-tk
03.11.2022 12:23Для клиентских приложений конечно можно и похромать какое-то время, чисто чтобы юзер сохранил данные.
И сохранить сломанные данные, чтобы при загрузке этого битого файла упасть окончательно?
Не всё так однозначно (с)
ksbes
К сожалению принцип fail fast понимается так что падать надо действительно сразу, даже не озаботившись сбором сведений о текущем состоянии и формировании информативных сообщениях об ошибке.
Поэтому следование этому принципу порождает «дома с привидениями» — вроде работает, но вдруг пятницу 31-го чётного года падает — и не понятно почему (реальный случай!). И хрен докопаешься без целого квеста с тупиками и разветвлениями.
Так что «по-умолчанию» всё же препочтительнее fail safe (естественно с записями в лог, предупреждениями и т.п. — иначе это будет ignore) — именно так, например, работает javascript в браузерах. Падает, но оставляет следы в логе.
koropovskiy
Я резюмировал из статьи так: На проде fail safe, до прода - fail fast. С моим опытом вполне согласуется.
ksbes
Мой опыт подсказывает мне, что при подготовке к проду никто не будет переделовать все блоки try-catch, все лесенки if-else и/или switch обработки ошибок. Очень, очень редко кто заморачивается хоть что-то поправить — хоть в одном месте.
Так что — особенно для небольших команд — у меня правило: пишем сразу как в прод («сразу пишем хорошо» :) ). Иначе в прод отправляется «прототип» как он есть.
hssergey
Да, в мобильных приложениях это очень весело, когда приложение внезапно молча закрывается.
xpendence Автор
На этапе разработки, падение приложения, конечно, предпочтительнее. Тестировщик сразу лезет в логи и заводит багу на разработчика. В этом и смысл fail-fast! В продакшене же, конечно, приложение не должно упасть. Оно должно собрать все данные об ошибке, отправить их куда надо, а пользователю вывести сообщение, что данная фича сейчас недоступна. Короче говоря, fail-safe! - это fail-fast! + бизнес-обёртка для минимизации ущерба.
xpendence Автор
Почему же, fail-fast! - это, в первую очередь, о системе, которая останавливает свою работу при возникновении ошибки, с прекращением всякой бизнес-логики. И не более. Обработка ошибки с последующим логированием стектрейса, возвращением негативного HTTP-статуса и прочей фиксацией ошибки могут быть заложены при прекращении бизнес-логики на системном уровне (тот самый случай "громкого падения"). При этом, бизнес-логика при негативном сценарии не осуществляется.
Принцип же fail-safe! подразумевает именно бизнес-логику при негативном сценарии. В этом и их различие.
Приведу пример. Клиент стучится на сервер за данными и не получает их. При реализации fail-fast! на экране пользователя возникает сообщение об ошибке, что-то вроде "HTTP Status 500: java.util.NoSuchElementException: Document not found..." и так далее. При этом, приложение упало как можно громче, с логами, аварийными записями в базу и так далее - но оно именно упало и больше ничего. Пользователь видит сообщение, морщится, но ничего, стерпит.
В случае же применения fail-safe! произойдёт такое же громкое падение с логами и прочим, но на клиент будет возвращено сообщение: "Спокойно, нет поводов для паники, мы не можем отыскать сервер, но обязательно его отыщем! Приходите завтра." Во втором случае, в негативный сценарий заложена дополнительная бизнес-логика, которая призвана сгладить пользователю негативный эффект.
А так, конечно, Вы правы - приложение должно падать как можно громче, причём, всегда.