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-ти минут ваше приложение всё же вернётся к обычной работе. Но это определённо ошибка для конечного пользователя.
(**) Подробнее см. тут.

2 комментария :

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

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

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

    ОтветитьУдалить

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

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

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

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

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