четверг, 19 мая 2016 г.

[prog.c++] Еще один real-world пример из SObjectizer: трюки с таймерами

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

Есть такая маленькая, но хорошая библиотечка procxx для запуска дочерних процессов в Unix-ах (я ее слегка доработал напильником, но мой pull request пока не приняли). Запуск какой-нибудь внешней программы и чтение ее выхлопа посредством procxx -- это не просто, а очень просто:

procxx::process some_tool( "some_tool", key, value, key2, value2, ... );
some_tool.exec();

std::string line;
while( std::getline(some_tool.output(), line) )
   handle_line(line);

some_tool.wait();

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

Сделать это дедлайн в SO-5 не сложнее, чем запустить внешний процесс с помощью procxx ;)

Самое тривиальное -- это агент, который будет отсылать сигнал SIGKILL слишком долго работающему процессу:

/*
 * Задача этого агента -- работать на отдельном контексте и насильно прерывать
 * работу процесса с указанным pid-ом.
 */
class a_child_deadliner_t final : public so_5::agent_t
{
public :
   a_child_deadliner_t( context_t ctx, spdlog::logger & logger )
      :  so_5::agent_t( ctx ), m_logger( logger )
   {
      so_subscribe_self().event( &a_child_deadliner_t::evt_deadline );
   }
   
   // Сообщение о том, что дедлайн для процесса наступил и процесс,
   // если информация о нем еще осталась, должен быть уничтожен.
   struct msg_deadline : public so_5::message_t
   {
      pid_t m_pid;
      const std::string m_description;

      msg_deadline( pid_t pid, std::string description )
         :  m_pid( pid ),  m_description( std::move(description) )
      {}
   };

private :
   spdlog::logger & m_logger;

   void evt_deadline( const msg_deadline & cmd )
   {
      m_logger.error( "deadline for process [pid={}][description={}]",
            cmd.m_pid, cmd.m_description );
      kill( cmd.m_pid, SIGKILL );
   }
};

А все самое интересное происходит в другом классе, process_deadline_t. Хотя он небольшой по объему, но его логика становится лучше понятна, если сначала показать, как он используется. Используется он так:

procxx::process some_tool( "some_tool", key, value, key2, value2, ... );
some_tool.exec();

process_deadline_t some_tool_deadline( env, deadliner_mbox,
      some_tool.id(), // PID процесса для остановки.
      std::chrono::seconds(15), // Сколько времени даем на работу.
      "some_tool is working too long" // Это будет сохранено в лог.
);

std::string line;
while( std::getline(some_tool.output(), line) )
   handle_line(line);

some_tool.wait();

Т.е. экземпляр process_deadline_t создается сразу после запуска процесса и живет все время, пока работает дочерний процесс. В своем конструкторе process_deadline_t отсылает агенту a_child_deadliner_t отложенное сообщение msg_deadline. А в деструкторе process_deadline_t происходит отмена этого сообщения. Т.о., если дочерний процесс завершился быстро, то process_deadline_t разрушится еще до того, как отложенное msg_deadline будет доставлено агенту a_child_deadliner_t.. А если нет, то сообщение msg_deadline дойдет до агента a_child_deadliner_t и тот отправит дочернему процессу сигнал SIGTERM.

Однако, вручную деструктор process_deadline_t описывать не нужно. Он будет сгенерирован автоматически, так что нужно всего лишь должным образом описать конструктор. Ну и, для того, чтобы при использовании process_deadline_t было меньше сюрпризов, данный класс является Moveable, но не Copyable.

А трюк заключается в том, что отложенное сообщение отменяется автоматически, когда разрушается последний timer_id для этого сообщения. Единственный timer_id хранится в process_deadline_t. Поэтому когда разрушается process_deadline_t, разрушается и timer_id и происходит отмена отложенного сообщения. Так что все работа -- это вызвать подходящий send в конструкторе process_deadline_t.

class process_deadline_t
{
   process_deadline_t( const process_deadline_t & ) = delete;

public :
   process_deadline_t(
      so_5::environment_t & env,
      // Почтовый ящик, на который нужно отослать сообщение об отмене.
      const so_5::mbox_t & deadliner_mbox,
      // Процесс, за временем жизни которого идет слежение.
      pid_t pid,
      // Сколько времени процессу разрешено работать.
      std::chrono::seconds deadline,
      // Пояснение для записи в лог при принудительном завершении
      // работы процесса
      std::string description )
      :  m_timer( so_5::send_periodic< a_child_deadliner_t::msg_deadline >(
               env,
               deadliner_mbox,
               deadline,
               std::chrono::seconds::zero(),
               pid,
               std::move(description) ) )
   {}

   process_deadline_t( process_deadline_t && other ) = default;

   process_deadline_t &
   operator=( process_deadline_t && o ) = default;

private :
   so_5::timer_id_t m_timer;
};

PS. Еще одно маленькое пояснение: используется send_periodic, а не send_delayed, потому, что send_delayed не возвращает timer_id. Из-за этого отосланное через send_delayed отложенное сообщение отменить нельзя. А вот send_periodic возвращает timer_id. И если при вызове send_periodic задать только pause, но оставить нулевой period (как в примере выше), то send_periodic работает так же, как и send_delayed.

Комментариев нет: