27 января 2009 г.

Anti-freeze в EurekaLog

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

Например, что если ваше приложение повисло из-за взаимной блокировки? Как насчёт бесконечного цикла, загружающего ваше ядро CPU на все 100%? А не перерисовывать пользовательский интерфейс 10 минут - это как? Эти ситуации являются багами или нет?

Да, все эти случаи - ошибки в вашем приложении (*). Но при этом в них не участвуют никакие исключения - ваше приложение просто тихо повисает. Такие ситуации тяжело анализировать, потому что приложение не показывает никакого сообщения об ошибке, не генерирует логов и т.п.

Итак: как же тогда вам их поймать?

Окей, если вы посмотрите в опции проекта EurekaLog, то на вкладке “Advanced Options” вы увидите нечто, под названием “Anti-Freeze Options”:

Вкладка “Advanced Options” в EurekaLog.

Изучив документацию EL, вы обнаружите, что при включении этой опции EurekaLog будет определять, не подвисло ли ваше приложение. Что это означает в точности и как работает? Кое-кто наверняка скажет: “во, это крутая фишка! Надо её обязательно включить!”, включит опцию, а потом столкнётся с загадочным поведением своего приложения. Хорошо бы понимать, что вы делаете, перед её включением.

Дело в том, что нет чёткого определения "зависшего приложения". Обычно, когда говорят, что приложение повисло, имеют ввиду, что оно либо попало на deadlock, либо сильно занято какой-то работой. Но с точки зрения конечного пользователя это всего лишь приложение которое перестало работать - следовательно, это баг в программе.

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

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

Что означает, что приложение висит? Блокировка, бесконечный цикл, остановившаяся прорисовка - все эти случаи объединяет общий момент: в этих ситуациях приложение не обрабатывает оконные сообщения - оно занято какими-то действиями, но не вызывает Peek/GetMessage. Поэтому, вы можете отправить сообщение WM_NULL вашему приложению, чтобы посмотреть: а ответит ли оно в отведённое время. Если вы не получили ответа за, скажем, 30-ть секунд - вы можете сказать, что приложение, вероятно, повисло. Обратите внимание на слово "вероятно". В действительности приложение может не висеть, а быть занятым чем-то (например, пытаться к кому-то достучаться по сети) - поэтому оно всегда оживёт, но позднее. Да, с технической точки зрения это не баг. Но, опять-же, конечного пользователя всё это не волнует. Его приложение не отвечает - значит, это баг. Точка.

При старте приложения EurekaLog запускает дополнительный служебный поток (TFreezeThread). Этот поток постоянно опрашивает главный поток вашего приложения с помощью сообщений WM_NULL. Если вы не сможете ответить за указанное время - тогда служебный поток посчитает вас зависшим и возбудит исключение класса EFrozenApplication. Поскольку это исключение, то оно может быть перехвачено стандартными средствами EurekaLog. EurekaLog при этом создаст полный отчёт, который может быть отправлен вам, вы его проанализируете и найдёте причину. По-умолчанию, приложение не завершается после закрытия диалога об ошибке. Поэтому, если приложение всё ещё висит - вы увидите ещё одно сообщение. И ещё одно. А потом появится опция перезапуска приложения:


Если вам не нравится такое поведение по-умолчанию - вы всегда можете изменить его. Лично я предпочитаю добавлять фильтр (в опциях проекта EL есть вкладка “Exceptions Filters”) на EFrozenApplication с указанием действия “Restart” или “Terminate”.

Важно отметить, что исключение возбуждается именно в главном потоке (а не мониторящем потоке) - поэтому стек вызова в отчёте поможет вам идентифицировать проблему. Ну, вообще говоря, возбуждение исключения в чужом потоке - это грязный трюк, включающий в себя прямые манипуляции контекстом потока и потенциально могущие привести к утечкам памяти (**) - поэтому не стоит использовать таких техники в штатных ситуациях. Но это отлично подходит для таких экстренных случаев - уж лучше иметь отчёт об ошибке + маловероятную утечку памяти, чем вообще ничего.

Кстати, вы можете включать anti-freeze и программным способом! Например, если вы хотите сделать что-то сильно тяжёлое в своём Button3Click, вы можете временно отключить anti-freeze и включить его после завершения действий. Для этого вам нужно поиграться со свойством CurrentEurekaLogOptions.FreezeActivate property. Сброс его в False завершит поток-монитор, а установка в True - создаст новый:
procedure TForm1.Button3Click(Sender: TObject);
begin
  CurrentEurekaLogOptions.FreezeActivate := False;
  try
    // <- а тут ваш код
  finally   
    CurrentEurekaLogOptions.FreezeActivate := True; 
  end; 
end;
Всё это конечно хорошо, но что же в конечном итоге для нас означает включение этой опции?

Первое: ваше приложение будет периодически получать сообщения WM_NULL (код которых равен 0). Именно так EurekaLog проверяет, живы ли вы. Вообще говоря, вас это не должно заботить, т.к. вы и не должны его никак обрабатывать.

Второе: вам теперь нужно очень аккуратно писать ваше приложение. Если вы напишете в свой Button1Click "плохой" код (а под "плохим" я понимаю код, который длительно выполняется) - в вашем приложении появится "баг". Если у вас есть код, который потенциально может выполняться долгое время (подключение к БД, например) - самое время вынести его в отдельный поток (и не забудьте поставить ему ограничение на время выполнения).

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

Четвёртое: контроль anti-freeze включается только после создания главной формы. Выполнение затяжных действий при загрузке - обычное явление. Поэтому EurekaLog ждёт, пока вы не проинициализируетесь, и только потом включает anti-freeze.

Пятое: ничего не бывает задаром и в нашем случае тоже есть незначительные накладные расходы. А именно: a) в вашем приложении появляется дополнительный поток и b) он постоянно опрашивает ваш главный поток. “Постоянно” означает несколько раз в секунду (конечно же, EL вовсе не хочет отжирать 100% вашего CPU только для того, чтобы проверить вашу активность :) ). С точки зрения CPU этот вспомогательный поток спит почти всегда. Поэтому я не знаю, кого вообще могут волновать такие мелочи, но на всякий случай я упоминаю о них.

Шестое: важно не путать anti-freeze EurekaLog с, скажем, компонентом TIdAntiFreeze - это совершенно разные вещи, которые не имеют ничего общего, кроме слегка похожего названия.

(*) Ну, последний случай (не прорисовка UI) формально не баг для вас, разработчика - поскольку после 10-ти минут ваше приложение всё же вернётся к обычной работе. Но это определённо ошибка для конечного пользователя.
(**) Подробнее см. тут.

6 комментариев :

  1. Интересная штука. Хотел спросить, намного ли она круче чем JCL-Exception dialog, потом почитал описание и понял, что намного.
    А не знаешь, в JCL случайно нет механизма для определения зависаний?

    ОтветитьУдалить
  2. В JCL я не встречал. Такое обычно есть в пакетах класса "всё-в-одном" - например, помимо EurekaLog такое есть ещё в madExcept.

    Ну а вообще, чисто по реализации - это весьма простая фишка. Всего-то и нужно, что написать один TThread с циклом, в котором вызывается SendMessageTimeout.

    ОтветитьУдалить
  3. А есть ли возможность запустить программно это поток в середине приложения, то есть не активировать опции при старте, а потом где-то в коде запустить этот поток? То есть имеет место ситуация, когда надо проверять на зависания только 1 кусок кода, без использования мониторинга на все приложение.

    ОтветитьУдалить
    Ответы
    1. Это очень странный вопрос.

      Если у вас есть кусок кода, который потенциально может приводить к зависаниям UI - ну так вынесите его в фоновый поток. А если вы не хотите этого делать - ну так вы уже знаете место, где будет зависание. Нафига вам тут проверка?

      Детект зависаний нужен в основном для того, чтобы найти места, о которых вы ещё не подозреваете.

      Но если хочется странного, то да, можно и ручками. В uses добавляйте EFreeze и вызывайтеего подпрограммы. Есть старт/стоп, равно как и просто пауза/возобновление.

      Удалить
  4. Все не так просто, место с ошибкой приблизительно в сторонней dll, через которую идет работа с устройством, там (судя по тому что видно при просмотре текста dll в текстовом редакторе) уже создается поток для работы с портом, и видимо используется критическая секция для блокировки ресурса. Ошибка плавающая, то есть ее просто никто не может повторить, её ловят иногда только конечные пользователи, которые при этом работают через "тонкого" клиента (ошибка проявляется только при работе через "тонкий" клиент). То есть приложение где-то на сервере крутится, а пользователи только смотрят у себя картинку с GUI. Воспроизвести ошибку для отладки вобщем-то нет возможности, выносить кучу обработки в отдельный поток как-то тоже пока не хочется, вот поэтому хотелось включить антифриз и ждать пока кто-то словит, потом лог хотя-бы посмотреть, где именно висит.
    EFreeze это я так понимаю из 7-й версии, которой у нас в той версии нашего ПО, в которой ошибка сейчас, не будет скорее всего никогда. Используется 6-я версия.

    ОтветитьУдалить
    Ответы
    1. Я про 6-ю уже не скажу, всё же шесть лет прошло уже. Но вон там в статье написано: CurrentEurekaLogOptions.FreezeActivate := True; Так что включайте в настройках, на старте - в False, перед кодом - в True.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.