пятница, 26 апреля 2013 г.

Крэши, вызванные исключениями

Оригинал

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

Однако все эти удобства имею стоимость, которую легко сформулировать:
Исключения учат разработчиков не заботиться об ошибках.

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

Реймонд Чен так описал эту проблему в 2004:

Написание корректного кода в модели с выбрасыванием исключений в некотором смысле труднее, чем в модели с возвращением кода ошибки, так как что угодно может потерпеть неудачу и Вы должны быть готовы к этому. В модели с возвращением кода ошибки, момент когда вы должны произвести проверку на наличие ошибок очевиден: как только вы получили код ошибки. В модели с исключениями Вы просто должны знать, что ошибки могут произойти в любом месте.
Другими словами, в модели с возвращением кода ошибки, когда кто-то пропускает обработку ошибки это происходит явно: они не проверяют код ошибки. В то же время в модели с выбрасыванием исключений при рассмотрении кода, в котором кто-то обрабатывает ошибку все не так ясно, так как ошибка не указана явно.
(…)
Когда Вы пишете код, задумываетесь ли Вы о том, каковы могут быть последствия каждого исключения, которое может возникнуть в каждой строчке кода? Вы должны делать это, если собираетесь писать корректный код.

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

Я не первый раз пишу об этом и, учитывая споры, которые окружают это заявление, поэтому я нашел пару примеров, которые подтверждают проблему. Лучший пример, который я смог найти на сегодняшний день находится в модуле pty стандартной библиотеки Python 3.3:

def spawn(argv, master_read=_read, stdin_read=_read):
    """Create a spawned process."""
    if type(argv) == type(''):
        argv = (argv,)
    pid, master_fd = fork()
    if pid == CHILD:
        os.execlp(argv[0], *argv)
    (...)

Каждый раз, когда кто-нибудь вызовет этот код с неправильным именем исполняемого файла в argv, будет порожден неиспользуемый, не подверженный сборки мусора и неизвестный приложению Python процесс, потому что execlp потерпит неудачу и форкнутый процесс будет проигнорирован. И будет ли клиент этого модуля ловить исключение или нет не имеет значения. Локальное обязательство не было выполнено. Конечно ошибка может быть исправлена тривиально добавлением try/except внутрь самой функции spawn. Однако, проблема в том, что это логика показалась нормальной всем, кто когда-либо видел эту функцию начиная с 1994 года, когда Гвидо ван Россум впервые закоммитил ее.

Вот другой интересный пример:

$ make clean
Sorry, command-not-found has crashed! Please file a bug report at:

https://bugs.launchpad.net/command-not-found/+filebug

Please include the following information with the report:

command-not-found version: 0.3
Python version: 3.2.3 final 0
Distributor ID: Ubuntu
Description:    Ubuntu 13.04
Release:        13.04
Codename:       raring
Exception information:

unsupported locale setting
Traceback (most recent call last):
  File "/.../CommandNotFound/util.py", line 24, in crash_guard
    callback()
  File "/usr/lib/command-not-found", line 69, in main
    enable_i18n()
  File "/usr/lib/command-not-found", line 40, in enable_i18n
    locale.setlocale(locale.LC_ALL, '')
  File "/usr/lib/python3.2/locale.py", line 541, in setlocale
    return _setlocale(category, locale)
locale.Error: unsupported locale setting

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

Такой "парашютный перехват" довольно распространен в исключение-оринетриованном программировании и это подход, как правило, дает разработчикам ложное чувство хорошей обработки ошибок в приложении. Вместо настоящей защиты приложения он становится просто удобным способом крэша. В данном случае, правильнее было бы вывести предупреждение, если это вообще необходимо, и позволить программе работать как обычно. Это можно было бы сделать простым оборачиванием вот этой строки:

try:
    locale.setlocale(locale.LC_ALL, '')
except Exception as e:
    print("Cannot change locale:", e)

Очевидно, это легко сделать. Но, опять же, проблема в том, что это было естественно не делать этого сразу. На самом деле, это более чем естественно: действительно кажется лучше не рассматривать ошибочный путь. В этом случае произойдет сокращение кода, он будет более прямолинейным, и в результате остается только тот, который приводит к желаемому результату.

В следствие этого, к сожалению, мы погружаемся в мир хрупкого программного обеспечения и розовых слонов. Хотя более выразительный стиль возвращения ошибок выстраивает правильное мышление: вернет ли функция или метод ошибку в результате? Как она будет обработана? Действительно ли функция взаимодействующая с системой не вернет ошибку? Как решается проблема, которая наверняка может возникнуть?

Удивительное количество крэшэй и просто непредсказуемое поведение является результатом такой непроизвольной небрежности.